diff --git a/.claude/commands/add-block.md b/.claude/commands/add-block.md
index 40f15e772f..12629d2ce6 100644
--- a/.claude/commands/add-block.md
+++ b/.claude/commands/add-block.md
@@ -1,11 +1,11 @@
---
-description: Create a block configuration for a Sim Studio integration with proper subBlocks, conditions, and tool wiring
+description: Create a block configuration for a Sim integration with proper subBlocks, conditions, and tool wiring
argument-hint:
---
# Add Block Skill
-You are an expert at creating block configurations for Sim Studio. You understand the serializer, subBlock types, conditions, dependsOn, modes, and all UI patterns.
+You are an expert at creating block configurations for Sim. You understand the serializer, subBlock types, conditions, dependsOn, modes, and all UI patterns.
## Your Task
diff --git a/.claude/commands/add-integration.md b/.claude/commands/add-integration.md
index 017bebcffb..9a8e3ca69b 100644
--- a/.claude/commands/add-integration.md
+++ b/.claude/commands/add-integration.md
@@ -1,11 +1,11 @@
---
-description: Add a complete integration to Sim Studio (tools, block, icon, registration)
+description: Add a complete integration to Sim (tools, block, icon, registration)
argument-hint: [api-docs-url]
---
# Add Integration Skill
-You are an expert at adding complete integrations to Sim Studio. This skill orchestrates the full process of adding a new service integration.
+You are an expert at adding complete integrations to Sim. This skill orchestrates the full process of adding a new service integration.
## Overview
diff --git a/.claude/commands/add-tools.md b/.claude/commands/add-tools.md
index d4dfee8706..a2857f73db 100644
--- a/.claude/commands/add-tools.md
+++ b/.claude/commands/add-tools.md
@@ -1,11 +1,11 @@
---
-description: Create tool configurations for a Sim Studio integration by reading API docs
+description: Create tool configurations for a Sim integration by reading API docs
argument-hint: [api-docs-url]
---
# Add Tools Skill
-You are an expert at creating tool configurations for Sim Studio integrations. Your job is to read API documentation and create properly structured tool files.
+You are an expert at creating tool configurations for Sim integrations. Your job is to read API documentation and create properly structured tool files.
## Your Task
diff --git a/.claude/commands/add-trigger.md b/.claude/commands/add-trigger.md
index 461563ab0c..b65edf42a6 100644
--- a/.claude/commands/add-trigger.md
+++ b/.claude/commands/add-trigger.md
@@ -1,11 +1,11 @@
---
-description: Create webhook triggers for a Sim Studio integration using the generic trigger builder
+description: Create webhook triggers for a Sim integration using the generic trigger builder
argument-hint:
---
# Add Trigger Skill
-You are an expert at creating webhook triggers for Sim Studio. You understand the trigger system, the generic `buildTriggerSubBlocks` helper, and how triggers connect to blocks.
+You are an expert at creating webhook triggers for Sim. You understand the trigger system, the generic `buildTriggerSubBlocks` helper, and how triggers connect to blocks.
## Your Task
diff --git a/.github/workflows/test-build.yml b/.github/workflows/test-build.yml
index cd9f480b68..fdf21d76fe 100644
--- a/.github/workflows/test-build.yml
+++ b/.github/workflows/test-build.yml
@@ -38,6 +38,41 @@ jobs:
- name: Install dependencies
run: bun install --frozen-lockfile
+ - name: Validate feature flags
+ run: |
+ FILE="apps/sim/lib/core/config/feature-flags.ts"
+ ERRORS=""
+
+ echo "Checking for hardcoded boolean feature flags..."
+
+ # Use perl for multiline matching to catch both:
+ # export const isHosted = true
+ # export const isHosted =
+ # true
+ HARDCODED=$(perl -0777 -ne 'while (/export const (is[A-Za-z]+)\s*=\s*\n?\s*(true|false)\b/g) { print " $1 = $2\n" }' "$FILE")
+
+ if [ -n "$HARDCODED" ]; then
+ ERRORS="${ERRORS}\n❌ Feature flags must not be hardcoded to boolean literals!\n\nFound hardcoded flags:\n${HARDCODED}\n\nFeature flags should derive their values from environment variables.\n"
+ fi
+
+ echo "Checking feature flag naming conventions..."
+
+ # Check that all export const (except functions) start with 'is'
+ # This finds exports like "export const someFlag" that don't start with "is" or "get"
+ BAD_NAMES=$(grep -E "^export const [a-z]" "$FILE" | grep -vE "^export const (is|get)" | sed 's/export const \([a-zA-Z]*\).*/ \1/')
+
+ if [ -n "$BAD_NAMES" ]; then
+ ERRORS="${ERRORS}\n❌ Feature flags must use 'is' prefix for boolean flags!\n\nFound incorrectly named flags:\n${BAD_NAMES}\n\nExample: 'hostedMode' should be 'isHostedMode'\n"
+ fi
+
+ if [ -n "$ERRORS" ]; then
+ echo ""
+ echo -e "$ERRORS"
+ exit 1
+ fi
+
+ echo "✅ All feature flags are properly configured"
+
- name: Lint code
run: bun run lint:check
diff --git a/CLAUDE.md b/CLAUDE.md
index c9621ff178..7f18cee1c4 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -1,4 +1,4 @@
-# Sim Studio Development Guidelines
+# Sim Development Guidelines
You are a professional software engineer. All code must follow best practices: accurate, readable, clean, and efficient.
diff --git a/README.md b/README.md
index abd3ed66fb..23c10de618 100644
--- a/README.md
+++ b/README.md
@@ -13,6 +13,10 @@
+
+
+
+
### Build Workflows with Ease
Design agent workflows visually on a canvas—connect agents, tools, and blocks, then run them instantly.
@@ -60,17 +64,11 @@ Docker must be installed and running on your machine.
### Self-hosted: Docker Compose
```bash
-# Clone the repository
-git clone https://github.com/simstudioai/sim.git
-
-# Navigate to the project directory
-cd sim
-
-# Start Sim
+git clone https://github.com/simstudioai/sim.git && cd sim
docker compose -f docker-compose.prod.yml up -d
```
-Access the application at [http://localhost:3000/](http://localhost:3000/)
+Open [http://localhost:3000](http://localhost:3000)
#### Using Local Models with Ollama
@@ -91,33 +89,17 @@ docker compose -f docker-compose.ollama.yml exec ollama ollama pull llama3.1:8b
#### Using an External Ollama Instance
-If you already have Ollama running on your host machine (outside Docker), you need to configure the `OLLAMA_URL` to use `host.docker.internal` instead of `localhost`:
+If Ollama is running on your host machine, use `host.docker.internal` instead of `localhost`:
```bash
-# Docker Desktop (macOS/Windows)
OLLAMA_URL=http://host.docker.internal:11434 docker compose -f docker-compose.prod.yml up -d
-
-# Linux (add extra_hosts or use host IP)
-docker compose -f docker-compose.prod.yml up -d # Then set OLLAMA_URL to your host's IP
```
-**Why?** When running inside Docker, `localhost` refers to the container itself, not your host machine. `host.docker.internal` is a special DNS name that resolves to the host.
-
-For Linux users, you can either:
-- Use your host machine's actual IP address (e.g., `http://192.168.1.100:11434`)
-- Add `extra_hosts: ["host.docker.internal:host-gateway"]` to the simstudio service in your compose file
+On Linux, use your host's IP address or add `extra_hosts: ["host.docker.internal:host-gateway"]` to the compose file.
#### Using vLLM
-Sim also supports [vLLM](https://docs.vllm.ai/) for self-hosted models with OpenAI-compatible API:
-
-```bash
-# Set these environment variables
-VLLM_BASE_URL=http://your-vllm-server:8000
-VLLM_API_KEY=your_optional_api_key # Only if your vLLM instance requires auth
-```
-
-When running with Docker, use `host.docker.internal` if vLLM is on your host machine (same as Ollama above).
+Sim supports [vLLM](https://docs.vllm.ai/) for self-hosted models. Set `VLLM_BASE_URL` and optionally `VLLM_API_KEY` in your environment.
### Self-hosted: Dev Containers
@@ -128,14 +110,9 @@ When running with Docker, use `host.docker.internal` if vLLM is on your host mac
### Self-hosted: Manual Setup
-**Requirements:**
-- [Bun](https://bun.sh/) runtime
-- [Node.js](https://nodejs.org/) v20+ (required for sandboxed code execution)
-- PostgreSQL 12+ with [pgvector extension](https://github.com/pgvector/pgvector) (required for AI embeddings)
-
-**Note:** Sim uses vector embeddings for AI features like knowledge bases and semantic search, which requires the `pgvector` PostgreSQL extension.
+**Requirements:** [Bun](https://bun.sh/), [Node.js](https://nodejs.org/) v20+, PostgreSQL 12+ with [pgvector](https://github.com/pgvector/pgvector)
-1. Clone and install dependencies:
+1. Clone and install:
```bash
git clone https://github.com/simstudioai/sim.git
@@ -145,75 +122,33 @@ bun install
2. Set up PostgreSQL with pgvector:
-You need PostgreSQL with the `vector` extension for embedding support. Choose one option:
-
-**Option A: Using Docker (Recommended)**
-```bash
-# Start PostgreSQL with pgvector extension
-docker run --name simstudio-db \
- -e POSTGRES_PASSWORD=your_password \
- -e POSTGRES_DB=simstudio \
- -p 5432:5432 -d \
- pgvector/pgvector:pg17
-```
-
-**Option B: Manual Installation**
-- Install PostgreSQL 12+ and the pgvector extension
-- See [pgvector installation guide](https://github.com/pgvector/pgvector#installation)
-
-3. Set up environment:
-
```bash
-cd apps/sim
-cp .env.example .env # Configure with required variables (DATABASE_URL, BETTER_AUTH_SECRET, BETTER_AUTH_URL)
+docker run --name simstudio-db -e POSTGRES_PASSWORD=your_password -e POSTGRES_DB=simstudio -p 5432:5432 -d pgvector/pgvector:pg17
```
-Update your `.env` file with the database URL:
-```bash
-DATABASE_URL="postgresql://postgres:your_password@localhost:5432/simstudio"
-```
-
-4. Set up the database:
-
-First, configure the database package environment:
-```bash
-cd packages/db
-cp .env.example .env
-```
+Or install manually via the [pgvector guide](https://github.com/pgvector/pgvector#installation).
-Update your `packages/db/.env` file with the database URL:
-```bash
-DATABASE_URL="postgresql://postgres:your_password@localhost:5432/simstudio"
-```
+3. Configure environment:
-Then run the migrations:
```bash
-cd packages/db # Required so drizzle picks correct .env file
-bunx drizzle-kit migrate --config=./drizzle.config.ts
+cp apps/sim/.env.example apps/sim/.env
+cp packages/db/.env.example packages/db/.env
+# Edit both .env files to set DATABASE_URL="postgresql://postgres:your_password@localhost:5432/simstudio"
```
-5. Start the development servers:
-
-**Recommended approach - run both servers together (from project root):**
+4. Run migrations:
```bash
-bun run dev:full
+cd packages/db && bunx drizzle-kit migrate --config=./drizzle.config.ts
```
-This starts both the main Next.js application and the realtime socket server required for full functionality.
-
-**Alternative - run servers separately:**
+5. Start development servers:
-Next.js app (from project root):
```bash
-bun run dev
+bun run dev:full # Starts both Next.js app and realtime socket server
```
-Realtime socket server (from `apps/sim` directory in a separate terminal):
-```bash
-cd apps/sim
-bun run dev:sockets
-```
+Or run separately: `bun run dev` (Next.js) and `cd apps/sim && bun run dev:sockets` (realtime).
## Copilot API Keys
@@ -224,7 +159,7 @@ Copilot is a Sim-managed service. To use Copilot on a self-hosted instance:
## Environment Variables
-Key environment variables for self-hosted deployments (see `apps/sim/.env.example` for full list):
+Key environment variables for self-hosted deployments. See [`.env.example`](apps/sim/.env.example) for defaults or [`env.ts`](apps/sim/lib/core/config/env.ts) for the full list.
| Variable | Required | Description |
|----------|----------|-------------|
@@ -232,9 +167,9 @@ Key environment variables for self-hosted deployments (see `apps/sim/.env.exampl
| `BETTER_AUTH_SECRET` | Yes | Auth secret (`openssl rand -hex 32`) |
| `BETTER_AUTH_URL` | Yes | Your app URL (e.g., `http://localhost:3000`) |
| `NEXT_PUBLIC_APP_URL` | Yes | Public app URL (same as above) |
-| `ENCRYPTION_KEY` | Yes | Encryption key (`openssl rand -hex 32`) |
-| `OLLAMA_URL` | No | Ollama server URL (default: `http://localhost:11434`) |
-| `VLLM_BASE_URL` | No | vLLM server URL for self-hosted models |
+| `ENCRYPTION_KEY` | Yes | Encrypts environment variables (`openssl rand -hex 32`) |
+| `INTERNAL_API_SECRET` | Yes | Encrypts internal API routes (`openssl rand -hex 32`) |
+| `API_ENCRYPTION_KEY` | Yes | Encrypts API keys (`openssl rand -hex 32`) |
| `COPILOT_API_KEY` | No | API key from sim.ai for Copilot features |
## Troubleshooting
diff --git a/apps/docs/components/icons.tsx b/apps/docs/components/icons.tsx
index 2c46036dbf..7addb30eaa 100644
--- a/apps/docs/components/icons.tsx
+++ b/apps/docs/components/icons.tsx
@@ -4078,6 +4078,31 @@ export function McpIcon(props: SVGProps) {
)
}
+export function A2AIcon(props: SVGProps) {
+ return (
+
+
+
+
+
+
+
+
+
+ )
+}
+
export function WordpressIcon(props: SVGProps) {
return (
diff --git a/apps/docs/components/ui/icon-mapping.ts b/apps/docs/components/ui/icon-mapping.ts
index 2258f7189e..fe03d578c1 100644
--- a/apps/docs/components/ui/icon-mapping.ts
+++ b/apps/docs/components/ui/icon-mapping.ts
@@ -4,6 +4,7 @@
import type { ComponentType, SVGProps } from 'react'
import {
+ A2AIcon,
AhrefsIcon,
AirtableIcon,
ApifyIcon,
@@ -127,6 +128,7 @@ import {
type IconComponent = ComponentType>
export const blockTypeToIconMap: Record = {
+ a2a: A2AIcon,
ahrefs: AhrefsIcon,
airtable: AirtableIcon,
apify: ApifyIcon,
diff --git a/apps/docs/content/docs/de/enterprise/index.mdx b/apps/docs/content/docs/de/enterprise/index.mdx
index 82682f260b..f81ff5a965 100644
--- a/apps/docs/content/docs/de/enterprise/index.mdx
+++ b/apps/docs/content/docs/de/enterprise/index.mdx
@@ -6,13 +6,13 @@ description: Enterprise-Funktionen für Organisationen mit erweiterten
import { Callout } from 'fumadocs-ui/components/callout'
-Sim Studio Enterprise bietet erweiterte Funktionen für Organisationen mit erhöhten Sicherheits-, Compliance- und Verwaltungsanforderungen.
+Sim Enterprise bietet erweiterte Funktionen für Organisationen mit erhöhten Sicherheits-, Compliance- und Verwaltungsanforderungen.
---
## Bring Your Own Key (BYOK)
-Verwenden Sie Ihre eigenen API-Schlüssel für KI-Modellanbieter anstelle der gehosteten Schlüssel von Sim Studio.
+Verwenden Sie Ihre eigenen API-Schlüssel für KI-Modellanbieter anstelle der gehosteten Schlüssel von Sim.
### Unterstützte Anbieter
@@ -33,7 +33,7 @@ Verwenden Sie Ihre eigenen API-Schlüssel für KI-Modellanbieter anstelle der ge
BYOK-Schlüssel werden verschlüsselt gespeichert. Nur Organisationsadministratoren und -inhaber können Schlüssel verwalten.
-Wenn konfiguriert, verwenden Workflows Ihren Schlüssel anstelle der gehosteten Schlüssel von Sim Studio. Bei Entfernung wechseln Workflows automatisch zu den gehosteten Schlüsseln zurück.
+Wenn konfiguriert, verwenden Workflows Ihren Schlüssel anstelle der gehosteten Schlüssel von Sim. Bei Entfernung wechseln Workflows automatisch zu den gehosteten Schlüsseln zurück.
---
@@ -73,5 +73,5 @@ Für selbst gehostete Bereitstellungen können Enterprise-Funktionen über Umgeb
| `DISABLE_INVITATIONS`, `NEXT_PUBLIC_DISABLE_INVITATIONS` | Workspace-/Organisations-Einladungen global deaktivieren |
- BYOK ist nur im gehosteten Sim Studio verfügbar. Selbst gehostete Deployments konfigurieren AI-Provider-Schlüssel direkt über Umgebungsvariablen.
+ BYOK ist nur im gehosteten Sim verfügbar. Selbst gehostete Deployments konfigurieren AI-Provider-Schlüssel direkt über Umgebungsvariablen.
diff --git a/apps/docs/content/docs/de/self-hosting/docker.mdx b/apps/docs/content/docs/de/self-hosting/docker.mdx
index 6623e7aabd..fe7ee620d3 100644
--- a/apps/docs/content/docs/de/self-hosting/docker.mdx
+++ b/apps/docs/content/docs/de/self-hosting/docker.mdx
@@ -1,6 +1,6 @@
---
title: Docker
-description: Sim Studio mit Docker Compose bereitstellen
+description: Sim mit Docker Compose bereitstellen
---
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
diff --git a/apps/docs/content/docs/de/self-hosting/environment-variables.mdx b/apps/docs/content/docs/de/self-hosting/environment-variables.mdx
index 4a1f4d34df..a64c6fc89e 100644
--- a/apps/docs/content/docs/de/self-hosting/environment-variables.mdx
+++ b/apps/docs/content/docs/de/self-hosting/environment-variables.mdx
@@ -1,6 +1,6 @@
---
title: Umgebungsvariablen
-description: Konfigurationsreferenz für Sim Studio
+description: Konfigurationsreferenz für Sim
---
import { Callout } from 'fumadocs-ui/components/callout'
diff --git a/apps/docs/content/docs/de/self-hosting/index.mdx b/apps/docs/content/docs/de/self-hosting/index.mdx
index e615ea09b7..7a5faa4d34 100644
--- a/apps/docs/content/docs/de/self-hosting/index.mdx
+++ b/apps/docs/content/docs/de/self-hosting/index.mdx
@@ -1,12 +1,12 @@
---
title: Self-Hosting
-description: Stellen Sie Sim Studio auf Ihrer eigenen Infrastruktur bereit
+description: Stellen Sie Sim auf Ihrer eigenen Infrastruktur bereit
---
import { Card, Cards } from 'fumadocs-ui/components/card'
import { Callout } from 'fumadocs-ui/components/callout'
-Stellen Sie Sim Studio auf Ihrer eigenen Infrastruktur mit Docker oder Kubernetes bereit.
+Stellen Sie Sim auf Ihrer eigenen Infrastruktur mit Docker oder Kubernetes bereit.
## Anforderungen
diff --git a/apps/docs/content/docs/de/self-hosting/kubernetes.mdx b/apps/docs/content/docs/de/self-hosting/kubernetes.mdx
index 3cdfd712f1..7f7a2a909e 100644
--- a/apps/docs/content/docs/de/self-hosting/kubernetes.mdx
+++ b/apps/docs/content/docs/de/self-hosting/kubernetes.mdx
@@ -1,6 +1,6 @@
---
title: Kubernetes
-description: Sim Studio mit Helm bereitstellen
+description: Sim mit Helm bereitstellen
---
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
diff --git a/apps/docs/content/docs/de/self-hosting/platforms.mdx b/apps/docs/content/docs/de/self-hosting/platforms.mdx
index ef34af4def..721383b2fa 100644
--- a/apps/docs/content/docs/de/self-hosting/platforms.mdx
+++ b/apps/docs/content/docs/de/self-hosting/platforms.mdx
@@ -1,6 +1,6 @@
---
title: Cloud-Plattformen
-description: Sim Studio auf Cloud-Plattformen bereitstellen
+description: Sim auf Cloud-Plattformen bereitstellen
---
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
@@ -64,7 +64,7 @@ sudo usermod -aG docker $USER
docker --version
```
-### Sim Studio bereitstellen
+### Sim bereitstellen
```bash
git clone https://github.com/simstudioai/sim.git && cd sim
diff --git a/apps/docs/content/docs/en/enterprise/index.mdx b/apps/docs/content/docs/en/enterprise/index.mdx
index 29abe99844..b1ad1993c5 100644
--- a/apps/docs/content/docs/en/enterprise/index.mdx
+++ b/apps/docs/content/docs/en/enterprise/index.mdx
@@ -5,7 +5,7 @@ description: Enterprise features for business organizations
import { Callout } from 'fumadocs-ui/components/callout'
-Sim Studio Enterprise provides advanced features for organizations with enhanced security, compliance, and management requirements.
+Sim Enterprise provides advanced features for organizations with enhanced security, compliance, and management requirements.
---
diff --git a/apps/docs/content/docs/en/execution/costs.mdx b/apps/docs/content/docs/en/execution/costs.mdx
index a376a97bb7..54c8a5dda5 100644
--- a/apps/docs/content/docs/en/execution/costs.mdx
+++ b/apps/docs/content/docs/en/execution/costs.mdx
@@ -106,7 +106,7 @@ The model breakdown shows:
## Bring Your Own Key (BYOK)
-Use your own API keys for AI model providers instead of Sim Studio's hosted keys to pay base prices with no markup.
+Use your own API keys for AI model providers instead of Sim's hosted keys to pay base prices with no markup.
### Supported Providers
@@ -127,7 +127,7 @@ Use your own API keys for AI model providers instead of Sim Studio's hosted keys
BYOK keys are encrypted at rest. Only workspace admins can manage keys.
-When configured, workflows use your key instead of Sim Studio's hosted keys. If removed, workflows automatically fall back to hosted keys with the multiplier.
+When configured, workflows use your key instead of Sim's hosted keys. If removed, workflows automatically fall back to hosted keys with the multiplier.
## Cost Optimization Strategies
diff --git a/apps/docs/content/docs/en/self-hosting/docker.mdx b/apps/docs/content/docs/en/self-hosting/docker.mdx
index 4ee0450bf0..846e6922f5 100644
--- a/apps/docs/content/docs/en/self-hosting/docker.mdx
+++ b/apps/docs/content/docs/en/self-hosting/docker.mdx
@@ -1,6 +1,6 @@
---
title: Docker
-description: Deploy Sim Studio with Docker Compose
+description: Deploy Sim with Docker Compose
---
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
diff --git a/apps/docs/content/docs/en/self-hosting/environment-variables.mdx b/apps/docs/content/docs/en/self-hosting/environment-variables.mdx
index 6b105211ad..327b0657a1 100644
--- a/apps/docs/content/docs/en/self-hosting/environment-variables.mdx
+++ b/apps/docs/content/docs/en/self-hosting/environment-variables.mdx
@@ -1,6 +1,6 @@
---
title: Environment Variables
-description: Configuration reference for Sim Studio
+description: Configuration reference for Sim
---
import { Callout } from 'fumadocs-ui/components/callout'
diff --git a/apps/docs/content/docs/en/self-hosting/index.mdx b/apps/docs/content/docs/en/self-hosting/index.mdx
index 63ef741914..87ab39b9a0 100644
--- a/apps/docs/content/docs/en/self-hosting/index.mdx
+++ b/apps/docs/content/docs/en/self-hosting/index.mdx
@@ -1,12 +1,18 @@
---
title: Self-Hosting
-description: Deploy Sim Studio on your own infrastructure
+description: Deploy Sim on your own infrastructure
---
import { Card, Cards } from 'fumadocs-ui/components/card'
import { Callout } from 'fumadocs-ui/components/callout'
-Deploy Sim Studio on your own infrastructure with Docker or Kubernetes.
+Deploy Sim on your own infrastructure with Docker or Kubernetes.
+
+
## Requirements
@@ -48,3 +54,4 @@ Open [http://localhost:3000](http://localhost:3000)
| realtime | 3002 | WebSocket server |
| db | 5432 | PostgreSQL with pgvector |
| migrations | - | Database migrations (runs once) |
+
diff --git a/apps/docs/content/docs/en/self-hosting/kubernetes.mdx b/apps/docs/content/docs/en/self-hosting/kubernetes.mdx
index da7a78970a..f12342198a 100644
--- a/apps/docs/content/docs/en/self-hosting/kubernetes.mdx
+++ b/apps/docs/content/docs/en/self-hosting/kubernetes.mdx
@@ -1,6 +1,6 @@
---
title: Kubernetes
-description: Deploy Sim Studio with Helm
+description: Deploy Sim with Helm
---
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
diff --git a/apps/docs/content/docs/en/self-hosting/platforms.mdx b/apps/docs/content/docs/en/self-hosting/platforms.mdx
index bcc3e7a092..d17455a877 100644
--- a/apps/docs/content/docs/en/self-hosting/platforms.mdx
+++ b/apps/docs/content/docs/en/self-hosting/platforms.mdx
@@ -1,6 +1,6 @@
---
title: Cloud Platforms
-description: Deploy Sim Studio on cloud platforms
+description: Deploy Sim on cloud platforms
---
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
@@ -57,7 +57,7 @@ sudo usermod -aG docker $USER
docker --version
```
-### Deploy Sim Studio
+### Deploy Sim
```bash
git clone https://github.com/simstudioai/sim.git && cd sim
diff --git a/apps/docs/content/docs/en/tools/a2a.mdx b/apps/docs/content/docs/en/tools/a2a.mdx
new file mode 100644
index 0000000000..558f1f907e
--- /dev/null
+++ b/apps/docs/content/docs/en/tools/a2a.mdx
@@ -0,0 +1,215 @@
+---
+title: A2A
+description: Interact with external A2A-compatible agents
+---
+
+import { BlockInfoCard } from "@/components/ui/block-info-card"
+
+
+
+{/* MANUAL-CONTENT-START:intro */}
+The A2A (Agent-to-Agent) protocol enables Sim to interact with external AI agents and systems that implement A2A-compatible APIs. With A2A, you can connect Sim’s automations and workflows to remote agents—such as LLM-powered bots, microservices, and other AI-based tools—using a standardized messaging format.
+
+Using the A2A tools in Sim, you can:
+
+- **Send Messages to External Agents**: Communicate directly with remote agents, providing prompts, commands, or data.
+- **Receive and Stream Responses**: Get structured responses, artifacts, or real-time updates from the agent as the task progresses.
+- **Continue Conversations or Tasks**: Carry on multi-turn conversations or workflows by referencing task and context IDs.
+- **Integrate Third-Party AI and Automation**: Leverage external A2A-compatible services as part of your Sim workflows.
+
+These features allow you to build advanced workflows that combine Sim’s native capabilities with the intelligence and automation of external AIs or custom agents. To use A2A integrations, you’ll need the external agent’s endpoint URL and, if required, an API key or credentials.
+{/* MANUAL-CONTENT-END */}
+
+
+## Usage Instructions
+
+Use the A2A (Agent-to-Agent) protocol to interact with external AI agents.
+
+
+
+## Tools
+
+### `a2a_send_message`
+
+Send a message to an external A2A-compatible agent.
+
+#### Input
+
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `agentUrl` | string | Yes | The A2A agent endpoint URL |
+| `message` | string | Yes | Message to send to the agent |
+| `taskId` | string | No | Task ID for continuing an existing task |
+| `contextId` | string | No | Context ID for conversation continuity |
+| `apiKey` | string | No | API key for authentication |
+
+#### Output
+
+| Parameter | Type | Description |
+| --------- | ---- | ----------- |
+| `content` | string | The text response from the agent |
+| `taskId` | string | Task ID for follow-up interactions |
+| `contextId` | string | Context ID for conversation continuity |
+| `state` | string | Task state |
+| `artifacts` | array | Structured output artifacts |
+| `history` | array | Full message history |
+
+### `a2a_get_task`
+
+Query the status of an existing A2A task.
+
+#### Input
+
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `agentUrl` | string | Yes | The A2A agent endpoint URL |
+| `taskId` | string | Yes | Task ID to query |
+| `apiKey` | string | No | API key for authentication |
+| `historyLength` | number | No | Number of history messages to include |
+
+#### Output
+
+| Parameter | Type | Description |
+| --------- | ---- | ----------- |
+| `taskId` | string | Task ID |
+| `contextId` | string | Context ID |
+| `state` | string | Task state |
+| `artifacts` | array | Output artifacts |
+| `history` | array | Message history |
+
+### `a2a_cancel_task`
+
+Cancel a running A2A task.
+
+#### Input
+
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `agentUrl` | string | Yes | The A2A agent endpoint URL |
+| `taskId` | string | Yes | Task ID to cancel |
+| `apiKey` | string | No | API key for authentication |
+
+#### Output
+
+| Parameter | Type | Description |
+| --------- | ---- | ----------- |
+| `cancelled` | boolean | Whether cancellation was successful |
+| `state` | string | Task state after cancellation |
+
+### `a2a_get_agent_card`
+
+Fetch the Agent Card (discovery document) for an A2A agent.
+
+#### Input
+
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `agentUrl` | string | Yes | The A2A agent endpoint URL |
+| `apiKey` | string | No | API key for authentication \(if required\) |
+
+#### Output
+
+| Parameter | Type | Description |
+| --------- | ---- | ----------- |
+| `name` | string | Agent name |
+| `description` | string | Agent description |
+| `url` | string | Agent endpoint URL |
+| `version` | string | Agent version |
+| `capabilities` | object | Agent capabilities \(streaming, pushNotifications, etc.\) |
+| `skills` | array | Skills the agent can perform |
+| `defaultInputModes` | array | Default input modes \(text, file, data\) |
+| `defaultOutputModes` | array | Default output modes \(text, file, data\) |
+
+### `a2a_resubscribe`
+
+Reconnect to an ongoing A2A task stream after connection interruption.
+
+#### Input
+
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `agentUrl` | string | Yes | The A2A agent endpoint URL |
+| `taskId` | string | Yes | Task ID to resubscribe to |
+| `apiKey` | string | No | API key for authentication |
+
+#### Output
+
+| Parameter | Type | Description |
+| --------- | ---- | ----------- |
+| `taskId` | string | Task ID |
+| `contextId` | string | Context ID |
+| `state` | string | Current task state |
+| `isRunning` | boolean | Whether the task is still running |
+| `artifacts` | array | Output artifacts |
+| `history` | array | Message history |
+
+### `a2a_set_push_notification`
+
+Configure a webhook to receive task update notifications.
+
+#### Input
+
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `agentUrl` | string | Yes | The A2A agent endpoint URL |
+| `taskId` | string | Yes | Task ID to configure notifications for |
+| `webhookUrl` | string | Yes | HTTPS webhook URL to receive notifications |
+| `token` | string | No | Token for webhook validation |
+| `apiKey` | string | No | API key for authentication |
+
+#### Output
+
+| Parameter | Type | Description |
+| --------- | ---- | ----------- |
+| `url` | string | Configured webhook URL |
+| `token` | string | Token for webhook validation |
+| `success` | boolean | Whether configuration was successful |
+
+### `a2a_get_push_notification`
+
+Get the push notification webhook configuration for a task.
+
+#### Input
+
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `agentUrl` | string | Yes | The A2A agent endpoint URL |
+| `taskId` | string | Yes | Task ID to get notification config for |
+| `apiKey` | string | No | API key for authentication |
+
+#### Output
+
+| Parameter | Type | Description |
+| --------- | ---- | ----------- |
+| `url` | string | Configured webhook URL |
+| `token` | string | Token for webhook validation |
+| `exists` | boolean | Whether a push notification config exists |
+
+### `a2a_delete_push_notification`
+
+Delete the push notification webhook configuration for a task.
+
+#### Input
+
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `agentUrl` | string | Yes | The A2A agent endpoint URL |
+| `taskId` | string | Yes | Task ID to delete notification config for |
+| `pushNotificationConfigId` | string | No | Push notification configuration ID to delete \(optional - server can derive from taskId\) |
+| `apiKey` | string | No | API key for authentication |
+
+#### Output
+
+| Parameter | Type | Description |
+| --------- | ---- | ----------- |
+| `success` | boolean | Whether deletion was successful |
+
+
+
+## Notes
+
+- Category: `tools`
+- Type: `a2a`
diff --git a/apps/docs/content/docs/en/tools/meta.json b/apps/docs/content/docs/en/tools/meta.json
index e489efd205..ea445d4488 100644
--- a/apps/docs/content/docs/en/tools/meta.json
+++ b/apps/docs/content/docs/en/tools/meta.json
@@ -1,6 +1,7 @@
{
"pages": [
"index",
+ "a2a",
"ahrefs",
"airtable",
"apify",
diff --git a/apps/docs/content/docs/es/enterprise/index.mdx b/apps/docs/content/docs/es/enterprise/index.mdx
index 2137bb5366..1f1b253db7 100644
--- a/apps/docs/content/docs/es/enterprise/index.mdx
+++ b/apps/docs/content/docs/es/enterprise/index.mdx
@@ -6,13 +6,13 @@ description: Funciones enterprise para organizaciones con requisitos avanzados
import { Callout } from 'fumadocs-ui/components/callout'
-Sim Studio Enterprise proporciona funciones avanzadas para organizaciones con requisitos mejorados de seguridad, cumplimiento y gestión.
+Sim Enterprise proporciona funciones avanzadas para organizaciones con requisitos mejorados de seguridad, cumplimiento y gestión.
---
## Bring Your Own Key (BYOK)
-Usa tus propias claves API para proveedores de modelos de IA en lugar de las claves alojadas de Sim Studio.
+Usa tus propias claves API para proveedores de modelos de IA en lugar de las claves alojadas de Sim.
### Proveedores compatibles
@@ -33,7 +33,7 @@ Usa tus propias claves API para proveedores de modelos de IA en lugar de las cla
Las claves BYOK están cifradas en reposo. Solo los administradores y propietarios de la organización pueden gestionar las claves.
-Cuando está configurado, los flujos de trabajo usan tu clave en lugar de las claves alojadas de Sim Studio. Si se elimina, los flujos de trabajo vuelven automáticamente a las claves alojadas.
+Cuando está configurado, los flujos de trabajo usan tu clave en lugar de las claves alojadas de Sim. Si se elimina, los flujos de trabajo vuelven automáticamente a las claves alojadas.
---
@@ -73,5 +73,5 @@ Para implementaciones self-hosted, las funciones enterprise se pueden activar me
| `DISABLE_INVITATIONS`, `NEXT_PUBLIC_DISABLE_INVITATIONS` | Desactivar globalmente invitaciones a espacios de trabajo/organizaciones |
- BYOK solo está disponible en Sim Studio alojado. Las implementaciones autoalojadas configuran las claves de proveedor de IA directamente a través de variables de entorno.
+ BYOK solo está disponible en Sim alojado. Las implementaciones autoalojadas configuran las claves de proveedor de IA directamente a través de variables de entorno.
diff --git a/apps/docs/content/docs/es/self-hosting/docker.mdx b/apps/docs/content/docs/es/self-hosting/docker.mdx
index a177da8bc1..370ae4376a 100644
--- a/apps/docs/content/docs/es/self-hosting/docker.mdx
+++ b/apps/docs/content/docs/es/self-hosting/docker.mdx
@@ -1,6 +1,6 @@
---
title: Docker
-description: Despliega Sim Studio con Docker Compose
+description: Despliega Sim con Docker Compose
---
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
diff --git a/apps/docs/content/docs/es/self-hosting/environment-variables.mdx b/apps/docs/content/docs/es/self-hosting/environment-variables.mdx
index cce23f2cdf..bd9e04027f 100644
--- a/apps/docs/content/docs/es/self-hosting/environment-variables.mdx
+++ b/apps/docs/content/docs/es/self-hosting/environment-variables.mdx
@@ -1,6 +1,6 @@
---
title: Variables de entorno
-description: Referencia de configuración para Sim Studio
+description: Referencia de configuración para Sim
---
import { Callout } from 'fumadocs-ui/components/callout'
diff --git a/apps/docs/content/docs/es/self-hosting/index.mdx b/apps/docs/content/docs/es/self-hosting/index.mdx
index b1016aca46..a511b1963d 100644
--- a/apps/docs/content/docs/es/self-hosting/index.mdx
+++ b/apps/docs/content/docs/es/self-hosting/index.mdx
@@ -1,12 +1,12 @@
---
title: Autoalojamiento
-description: Despliega Sim Studio en tu propia infraestructura
+description: Despliega Sim en tu propia infraestructura
---
import { Card, Cards } from 'fumadocs-ui/components/card'
import { Callout } from 'fumadocs-ui/components/callout'
-Despliega Sim Studio en tu propia infraestructura con Docker o Kubernetes.
+Despliega Sim en tu propia infraestructura con Docker o Kubernetes.
## Requisitos
diff --git a/apps/docs/content/docs/es/self-hosting/kubernetes.mdx b/apps/docs/content/docs/es/self-hosting/kubernetes.mdx
index 426f21454f..9e559e793d 100644
--- a/apps/docs/content/docs/es/self-hosting/kubernetes.mdx
+++ b/apps/docs/content/docs/es/self-hosting/kubernetes.mdx
@@ -1,6 +1,6 @@
---
title: Kubernetes
-description: Desplegar Sim Studio con Helm
+description: Desplegar Sim con Helm
---
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
diff --git a/apps/docs/content/docs/es/self-hosting/platforms.mdx b/apps/docs/content/docs/es/self-hosting/platforms.mdx
index 2de93bbed3..1cbf28685d 100644
--- a/apps/docs/content/docs/es/self-hosting/platforms.mdx
+++ b/apps/docs/content/docs/es/self-hosting/platforms.mdx
@@ -1,6 +1,6 @@
---
title: Plataformas en la nube
-description: Despliega Sim Studio en plataformas en la nube
+description: Despliega Sim en plataformas en la nube
---
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
@@ -64,7 +64,7 @@ sudo usermod -aG docker $USER
docker --version
```
-### Desplegar Sim Studio
+### Desplegar Sim
```bash
git clone https://github.com/simstudioai/sim.git && cd sim
diff --git a/apps/docs/content/docs/fr/enterprise/index.mdx b/apps/docs/content/docs/fr/enterprise/index.mdx
index c3eb71122e..3da50d4760 100644
--- a/apps/docs/content/docs/fr/enterprise/index.mdx
+++ b/apps/docs/content/docs/fr/enterprise/index.mdx
@@ -6,13 +6,13 @@ description: Fonctionnalités entreprise pour les organisations ayant des
import { Callout } from 'fumadocs-ui/components/callout'
-Sim Studio Entreprise fournit des fonctionnalités avancées pour les organisations ayant des exigences renforcées en matière de sécurité, de conformité et de gestion.
+Sim Entreprise fournit des fonctionnalités avancées pour les organisations ayant des exigences renforcées en matière de sécurité, de conformité et de gestion.
---
## Apportez votre propre clé (BYOK)
-Utilisez vos propres clés API pour les fournisseurs de modèles IA au lieu des clés hébergées par Sim Studio.
+Utilisez vos propres clés API pour les fournisseurs de modèles IA au lieu des clés hébergées par Sim.
### Fournisseurs pris en charge
@@ -33,7 +33,7 @@ Utilisez vos propres clés API pour les fournisseurs de modèles IA au lieu des
Les clés BYOK sont chiffrées au repos. Seuls les administrateurs et propriétaires de l'organisation peuvent gérer les clés.
-Une fois configurés, les workflows utilisent votre clé au lieu des clés hébergées par Sim Studio. Si elle est supprimée, les workflows basculent automatiquement vers les clés hébergées.
+Une fois configurés, les workflows utilisent votre clé au lieu des clés hébergées par Sim. Si elle est supprimée, les workflows basculent automatiquement vers les clés hébergées.
---
@@ -73,5 +73,5 @@ Pour les déploiements auto-hébergés, les fonctionnalités entreprise peuvent
| `DISABLE_INVITATIONS`, `NEXT_PUBLIC_DISABLE_INVITATIONS` | Désactiver globalement les invitations aux espaces de travail/organisations |
- BYOK est uniquement disponible sur Sim Studio hébergé. Les déploiements auto-hébergés configurent les clés de fournisseur d'IA directement via les variables d'environnement.
+ BYOK est uniquement disponible sur Sim hébergé. Les déploiements auto-hébergés configurent les clés de fournisseur d'IA directement via les variables d'environnement.
diff --git a/apps/docs/content/docs/fr/self-hosting/docker.mdx b/apps/docs/content/docs/fr/self-hosting/docker.mdx
index 75d049fc73..d4cd1ac17d 100644
--- a/apps/docs/content/docs/fr/self-hosting/docker.mdx
+++ b/apps/docs/content/docs/fr/self-hosting/docker.mdx
@@ -1,6 +1,6 @@
---
title: Docker
-description: Déployer Sim Studio avec Docker Compose
+description: Déployer Sim avec Docker Compose
---
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
diff --git a/apps/docs/content/docs/fr/self-hosting/environment-variables.mdx b/apps/docs/content/docs/fr/self-hosting/environment-variables.mdx
index e9c542382e..dc428373a3 100644
--- a/apps/docs/content/docs/fr/self-hosting/environment-variables.mdx
+++ b/apps/docs/content/docs/fr/self-hosting/environment-variables.mdx
@@ -1,6 +1,6 @@
---
title: Variables d'environnement
-description: Référence de configuration pour Sim Studio
+description: Référence de configuration pour Sim
---
import { Callout } from 'fumadocs-ui/components/callout'
diff --git a/apps/docs/content/docs/fr/self-hosting/index.mdx b/apps/docs/content/docs/fr/self-hosting/index.mdx
index 0da91c1185..bb52a8aa83 100644
--- a/apps/docs/content/docs/fr/self-hosting/index.mdx
+++ b/apps/docs/content/docs/fr/self-hosting/index.mdx
@@ -1,12 +1,12 @@
---
title: Auto-hébergement
-description: Déployez Sim Studio sur votre propre infrastructure
+description: Déployez Sim sur votre propre infrastructure
---
import { Card, Cards } from 'fumadocs-ui/components/card'
import { Callout } from 'fumadocs-ui/components/callout'
-Déployez Sim Studio sur votre propre infrastructure avec Docker ou Kubernetes.
+Déployez Sim sur votre propre infrastructure avec Docker ou Kubernetes.
## Prérequis
diff --git a/apps/docs/content/docs/fr/self-hosting/kubernetes.mdx b/apps/docs/content/docs/fr/self-hosting/kubernetes.mdx
index 892064eafd..398a0d8b45 100644
--- a/apps/docs/content/docs/fr/self-hosting/kubernetes.mdx
+++ b/apps/docs/content/docs/fr/self-hosting/kubernetes.mdx
@@ -1,6 +1,6 @@
---
title: Kubernetes
-description: Déployer Sim Studio avec Helm
+description: Déployer Sim avec Helm
---
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
diff --git a/apps/docs/content/docs/fr/self-hosting/platforms.mdx b/apps/docs/content/docs/fr/self-hosting/platforms.mdx
index 694d23473a..826fe8fb07 100644
--- a/apps/docs/content/docs/fr/self-hosting/platforms.mdx
+++ b/apps/docs/content/docs/fr/self-hosting/platforms.mdx
@@ -1,6 +1,6 @@
---
title: Plateformes cloud
-description: Déployer Sim Studio sur des plateformes cloud
+description: Déployer Sim sur des plateformes cloud
---
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
@@ -64,7 +64,7 @@ sudo usermod -aG docker $USER
docker --version
```
-### Déployer Sim Studio
+### Déployer Sim
```bash
git clone https://github.com/simstudioai/sim.git && cd sim
diff --git a/apps/docs/content/docs/ja/enterprise/index.mdx b/apps/docs/content/docs/ja/enterprise/index.mdx
index 1fc8dd6c5a..701acaaf4e 100644
--- a/apps/docs/content/docs/ja/enterprise/index.mdx
+++ b/apps/docs/content/docs/ja/enterprise/index.mdx
@@ -5,13 +5,13 @@ description: 高度なセキュリティとコンプライアンス要件を持
import { Callout } from 'fumadocs-ui/components/callout'
-Sim Studio Enterpriseは、強化されたセキュリティ、コンプライアンス、管理要件を持つ組織向けの高度な機能を提供します。
+Sim Enterpriseは、強化されたセキュリティ、コンプライアンス、管理要件を持つ組織向けの高度な機能を提供します。
---
## Bring Your Own Key (BYOK)
-Sim Studioのホストキーの代わりに、AIモデルプロバイダー用の独自のAPIキーを使用できます。
+Simのホストキーの代わりに、AIモデルプロバイダー用の独自のAPIキーを使用できます。
### 対応プロバイダー
@@ -32,7 +32,7 @@ Sim Studioのホストキーの代わりに、AIモデルプロバイダー用
BYOKキーは保存時に暗号化されます。組織の管理者とオーナーのみがキーを管理できます。
-設定すると、ワークフローはSim Studioのホストキーの代わりに独自のキーを使用します。削除すると、ワークフローは自動的にホストキーにフォールバックします。
+設定すると、ワークフローはSimのホストキーの代わりに独自のキーを使用します。削除すると、ワークフローは自動的にホストキーにフォールバックします。
---
@@ -72,5 +72,5 @@ Sim Studioのホストキーの代わりに、AIモデルプロバイダー用
| `DISABLE_INVITATIONS`、`NEXT_PUBLIC_DISABLE_INVITATIONS` | ワークスペース/組織への招待をグローバルに無効化 |
- BYOKはホスト型Sim Studioでのみ利用可能です。セルフホスト型デプロイメントでは、環境変数を介してAIプロバイダーキーを直接設定します。
+ BYOKはホスト型Simでのみ利用可能です。セルフホスト型デプロイメントでは、環境変数を介してAIプロバイダーキーを直接設定します。
diff --git a/apps/docs/content/docs/ja/self-hosting/docker.mdx b/apps/docs/content/docs/ja/self-hosting/docker.mdx
index c39ba2fc1b..7832a494c5 100644
--- a/apps/docs/content/docs/ja/self-hosting/docker.mdx
+++ b/apps/docs/content/docs/ja/self-hosting/docker.mdx
@@ -1,6 +1,6 @@
---
title: Docker
-description: Docker Composeを使用してSim Studioをデプロイする
+description: Docker Composeを使用してSimをデプロイする
---
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
diff --git a/apps/docs/content/docs/ja/self-hosting/environment-variables.mdx b/apps/docs/content/docs/ja/self-hosting/environment-variables.mdx
index ccdb20de2c..bd3e2c5926 100644
--- a/apps/docs/content/docs/ja/self-hosting/environment-variables.mdx
+++ b/apps/docs/content/docs/ja/self-hosting/environment-variables.mdx
@@ -1,6 +1,6 @@
---
title: 環境変数
-description: Sim Studioの設定リファレンス
+description: Simの設定リファレンス
---
import { Callout } from 'fumadocs-ui/components/callout'
diff --git a/apps/docs/content/docs/ja/self-hosting/index.mdx b/apps/docs/content/docs/ja/self-hosting/index.mdx
index a97e7380f7..69ae41e6c2 100644
--- a/apps/docs/content/docs/ja/self-hosting/index.mdx
+++ b/apps/docs/content/docs/ja/self-hosting/index.mdx
@@ -1,12 +1,12 @@
---
title: セルフホスティング
-description: 自社のインフラストラクチャにSim Studioをデプロイ
+description: 自社のインフラストラクチャにSimをデプロイ
---
import { Card, Cards } from 'fumadocs-ui/components/card'
import { Callout } from 'fumadocs-ui/components/callout'
-DockerまたはKubernetesを使用して、自社のインフラストラクチャにSim Studioをデプロイします。
+DockerまたはKubernetesを使用して、自社のインフラストラクチャにSimをデプロイします。
## 要件
diff --git a/apps/docs/content/docs/ja/self-hosting/kubernetes.mdx b/apps/docs/content/docs/ja/self-hosting/kubernetes.mdx
index 95e1638694..e4e53fafce 100644
--- a/apps/docs/content/docs/ja/self-hosting/kubernetes.mdx
+++ b/apps/docs/content/docs/ja/self-hosting/kubernetes.mdx
@@ -1,6 +1,6 @@
---
title: Kubernetes
-description: Helmを使用してSim Studioをデプロイする
+description: Helmを使用してSimをデプロイする
---
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
diff --git a/apps/docs/content/docs/ja/self-hosting/platforms.mdx b/apps/docs/content/docs/ja/self-hosting/platforms.mdx
index b6779c4a63..7d43a5db92 100644
--- a/apps/docs/content/docs/ja/self-hosting/platforms.mdx
+++ b/apps/docs/content/docs/ja/self-hosting/platforms.mdx
@@ -1,6 +1,6 @@
---
title: クラウドプラットフォーム
-description: クラウドプラットフォームにSim Studioをデプロイする
+description: クラウドプラットフォームにSimをデプロイする
---
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
@@ -64,7 +64,7 @@ sudo usermod -aG docker $USER
docker --version
```
-### Sim Studioのデプロイ
+### Simのデプロイ
```bash
git clone https://github.com/simstudioai/sim.git && cd sim
diff --git a/apps/docs/content/docs/zh/enterprise/index.mdx b/apps/docs/content/docs/zh/enterprise/index.mdx
index edef31636b..5202105091 100644
--- a/apps/docs/content/docs/zh/enterprise/index.mdx
+++ b/apps/docs/content/docs/zh/enterprise/index.mdx
@@ -5,13 +5,13 @@ description: 为具有高级安全性和合规性需求的组织提供企业级
import { Callout } from 'fumadocs-ui/components/callout'
-Sim Studio 企业版为需要更高安全性、合规性和管理能力的组织提供高级功能。
+Sim 企业版为需要更高安全性、合规性和管理能力的组织提供高级功能。
---
## 自带密钥(BYOK)
-使用您自己的 API 密钥对接 AI 模型服务商,而不是使用 Sim Studio 托管的密钥。
+使用您自己的 API 密钥对接 AI 模型服务商,而不是使用 Sim 托管的密钥。
### 支持的服务商
@@ -32,7 +32,7 @@ Sim Studio 企业版为需要更高安全性、合规性和管理能力的组织
BYOK 密钥静态加密存储。仅组织管理员和所有者可管理密钥。
-配置后,工作流将使用您的密钥而非 Sim Studio 托管密钥。如移除,工作流会自动切换回托管密钥。
+配置后,工作流将使用您的密钥而非 Sim 托管密钥。如移除,工作流会自动切换回托管密钥。
---
@@ -72,5 +72,5 @@ Sim Studio 企业版为需要更高安全性、合规性和管理能力的组织
| `DISABLE_INVITATIONS`,`NEXT_PUBLIC_DISABLE_INVITATIONS` | 全局禁用工作区/组织邀请 |
- BYOK 仅适用于托管版 Sim Studio。自托管部署需通过环境变量直接配置 AI 提供商密钥。
+ BYOK 仅适用于托管版 Sim。自托管部署需通过环境变量直接配置 AI 提供商密钥。
diff --git a/apps/docs/content/docs/zh/self-hosting/docker.mdx b/apps/docs/content/docs/zh/self-hosting/docker.mdx
index 353487bbde..90900860c4 100644
--- a/apps/docs/content/docs/zh/self-hosting/docker.mdx
+++ b/apps/docs/content/docs/zh/self-hosting/docker.mdx
@@ -1,6 +1,6 @@
---
title: Docker
-description: 使用 Docker Compose 部署 Sim Studio
+description: 使用 Docker Compose 部署 Sim
---
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
diff --git a/apps/docs/content/docs/zh/self-hosting/environment-variables.mdx b/apps/docs/content/docs/zh/self-hosting/environment-variables.mdx
index 330e3b5b3f..212751ab9b 100644
--- a/apps/docs/content/docs/zh/self-hosting/environment-variables.mdx
+++ b/apps/docs/content/docs/zh/self-hosting/environment-variables.mdx
@@ -1,6 +1,6 @@
---
title: 环境变量
-description: Sim Studio 的配置参考
+description: Sim 的配置参考
---
import { Callout } from 'fumadocs-ui/components/callout'
diff --git a/apps/docs/content/docs/zh/self-hosting/index.mdx b/apps/docs/content/docs/zh/self-hosting/index.mdx
index 0a66f0ad47..a2a4f4e50e 100644
--- a/apps/docs/content/docs/zh/self-hosting/index.mdx
+++ b/apps/docs/content/docs/zh/self-hosting/index.mdx
@@ -1,12 +1,12 @@
---
title: 自托管
-description: 在您自己的基础设施上部署 Sim Studio
+description: 在您自己的基础设施上部署 Sim
---
import { Card, Cards } from 'fumadocs-ui/components/card'
import { Callout } from 'fumadocs-ui/components/callout'
-使用 Docker 或 Kubernetes 在您自己的基础设施上部署 Sim Studio。
+使用 Docker 或 Kubernetes 在您自己的基础设施上部署 Sim。
## 要求
diff --git a/apps/docs/content/docs/zh/self-hosting/kubernetes.mdx b/apps/docs/content/docs/zh/self-hosting/kubernetes.mdx
index 69c88e967c..7041aa2994 100644
--- a/apps/docs/content/docs/zh/self-hosting/kubernetes.mdx
+++ b/apps/docs/content/docs/zh/self-hosting/kubernetes.mdx
@@ -1,6 +1,6 @@
---
title: Kubernetes
-description: 使用 Helm 部署 Sim Studio
+description: 使用 Helm 部署 Sim
---
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
diff --git a/apps/docs/content/docs/zh/self-hosting/platforms.mdx b/apps/docs/content/docs/zh/self-hosting/platforms.mdx
index 5165cc5259..14c43fb9cc 100644
--- a/apps/docs/content/docs/zh/self-hosting/platforms.mdx
+++ b/apps/docs/content/docs/zh/self-hosting/platforms.mdx
@@ -1,6 +1,6 @@
---
title: 云平台
-description: 在云平台上部署 Sim Studio
+description: 在云平台上部署 Sim
---
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
@@ -64,7 +64,7 @@ sudo usermod -aG docker $USER
docker --version
```
-### 部署 Sim Studio
+### 部署 Sim
```bash
git clone https://github.com/simstudioai/sim.git && cd sim
diff --git a/apps/sim/app/(landing)/components/structured-data.tsx b/apps/sim/app/(landing)/components/structured-data.tsx
index 70d572c581..ba3d9e704e 100644
--- a/apps/sim/app/(landing)/components/structured-data.tsx
+++ b/apps/sim/app/(landing)/components/structured-data.tsx
@@ -6,7 +6,7 @@ export default function StructuredData() {
'@type': 'Organization',
'@id': 'https://sim.ai/#organization',
name: 'Sim',
- alternateName: 'Sim Studio',
+ alternateName: 'Sim',
description:
'Open-source AI agent workflow builder used by developers at trail-blazing startups to Fortune 500 companies',
url: 'https://sim.ai',
diff --git a/apps/sim/app/api/a2a/agents/[agentId]/route.ts b/apps/sim/app/api/a2a/agents/[agentId]/route.ts
new file mode 100644
index 0000000000..74c13af879
--- /dev/null
+++ b/apps/sim/app/api/a2a/agents/[agentId]/route.ts
@@ -0,0 +1,289 @@
+import { db } from '@sim/db'
+import { a2aAgent, workflow } from '@sim/db/schema'
+import { createLogger } from '@sim/logger'
+import { eq } from 'drizzle-orm'
+import { type NextRequest, NextResponse } from 'next/server'
+import { generateAgentCard, generateSkillsFromWorkflow } from '@/lib/a2a/agent-card'
+import type { AgentCapabilities, AgentSkill } from '@/lib/a2a/types'
+import { checkHybridAuth } from '@/lib/auth/hybrid'
+import { getRedisClient } from '@/lib/core/config/redis'
+import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils'
+
+const logger = createLogger('A2AAgentCardAPI')
+
+export const dynamic = 'force-dynamic'
+
+interface RouteParams {
+ agentId: string
+}
+
+/**
+ * GET - Returns the Agent Card for discovery
+ */
+export async function GET(request: NextRequest, { params }: { params: Promise }) {
+ const { agentId } = await params
+
+ try {
+ const [agent] = await db
+ .select({
+ agent: a2aAgent,
+ workflow: workflow,
+ })
+ .from(a2aAgent)
+ .innerJoin(workflow, eq(a2aAgent.workflowId, workflow.id))
+ .where(eq(a2aAgent.id, agentId))
+ .limit(1)
+
+ if (!agent) {
+ return NextResponse.json({ error: 'Agent not found' }, { status: 404 })
+ }
+
+ if (!agent.agent.isPublished) {
+ const auth = await checkHybridAuth(request, { requireWorkflowId: false })
+ if (!auth.success) {
+ return NextResponse.json({ error: 'Agent not published' }, { status: 404 })
+ }
+ }
+
+ const agentCard = generateAgentCard(
+ {
+ id: agent.agent.id,
+ name: agent.agent.name,
+ description: agent.agent.description,
+ version: agent.agent.version,
+ capabilities: agent.agent.capabilities as AgentCapabilities,
+ skills: agent.agent.skills as AgentSkill[],
+ },
+ {
+ id: agent.workflow.id,
+ name: agent.workflow.name,
+ description: agent.workflow.description,
+ }
+ )
+
+ return NextResponse.json(agentCard, {
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Cache-Control': agent.agent.isPublished ? 'public, max-age=3600' : 'private, no-cache',
+ },
+ })
+ } catch (error) {
+ logger.error('Error getting Agent Card:', error)
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
+ }
+}
+
+/**
+ * PUT - Update an agent
+ */
+export async function PUT(request: NextRequest, { params }: { params: Promise }) {
+ const { agentId } = await params
+
+ try {
+ const auth = await checkHybridAuth(request, { requireWorkflowId: false })
+ if (!auth.success || !auth.userId) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
+ }
+
+ const [existingAgent] = await db
+ .select()
+ .from(a2aAgent)
+ .where(eq(a2aAgent.id, agentId))
+ .limit(1)
+
+ if (!existingAgent) {
+ return NextResponse.json({ error: 'Agent not found' }, { status: 404 })
+ }
+
+ const body = await request.json()
+
+ if (
+ body.skillTags !== undefined &&
+ (!Array.isArray(body.skillTags) ||
+ !body.skillTags.every((tag: unknown): tag is string => typeof tag === 'string'))
+ ) {
+ return NextResponse.json({ error: 'skillTags must be an array of strings' }, { status: 400 })
+ }
+
+ let skills = body.skills ?? existingAgent.skills
+ if (body.skillTags !== undefined) {
+ const agentName = body.name ?? existingAgent.name
+ const agentDescription = body.description ?? existingAgent.description
+ skills = generateSkillsFromWorkflow(agentName, agentDescription, body.skillTags)
+ }
+
+ const [updatedAgent] = await db
+ .update(a2aAgent)
+ .set({
+ name: body.name ?? existingAgent.name,
+ description: body.description ?? existingAgent.description,
+ version: body.version ?? existingAgent.version,
+ capabilities: body.capabilities ?? existingAgent.capabilities,
+ skills,
+ authentication: body.authentication ?? existingAgent.authentication,
+ isPublished: body.isPublished ?? existingAgent.isPublished,
+ publishedAt:
+ body.isPublished && !existingAgent.isPublished ? new Date() : existingAgent.publishedAt,
+ updatedAt: new Date(),
+ })
+ .where(eq(a2aAgent.id, agentId))
+ .returning()
+
+ logger.info(`Updated A2A agent: ${agentId}`)
+
+ return NextResponse.json({ success: true, agent: updatedAgent })
+ } catch (error) {
+ logger.error('Error updating agent:', error)
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
+ }
+}
+
+/**
+ * DELETE - Delete an agent
+ */
+export async function DELETE(request: NextRequest, { params }: { params: Promise }) {
+ const { agentId } = await params
+
+ try {
+ const auth = await checkHybridAuth(request, { requireWorkflowId: false })
+ if (!auth.success || !auth.userId) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
+ }
+
+ const [existingAgent] = await db
+ .select()
+ .from(a2aAgent)
+ .where(eq(a2aAgent.id, agentId))
+ .limit(1)
+
+ if (!existingAgent) {
+ return NextResponse.json({ error: 'Agent not found' }, { status: 404 })
+ }
+
+ await db.delete(a2aAgent).where(eq(a2aAgent.id, agentId))
+
+ logger.info(`Deleted A2A agent: ${agentId}`)
+
+ return NextResponse.json({ success: true })
+ } catch (error) {
+ logger.error('Error deleting agent:', error)
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
+ }
+}
+
+/**
+ * POST - Publish/unpublish an agent
+ */
+export async function POST(request: NextRequest, { params }: { params: Promise }) {
+ const { agentId } = await params
+
+ try {
+ const auth = await checkHybridAuth(request, { requireWorkflowId: false })
+ if (!auth.success || !auth.userId) {
+ logger.warn('A2A agent publish auth failed:', { error: auth.error, hasUserId: !!auth.userId })
+ return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
+ }
+
+ const [existingAgent] = await db
+ .select()
+ .from(a2aAgent)
+ .where(eq(a2aAgent.id, agentId))
+ .limit(1)
+
+ if (!existingAgent) {
+ return NextResponse.json({ error: 'Agent not found' }, { status: 404 })
+ }
+
+ const body = await request.json()
+ const action = body.action as 'publish' | 'unpublish' | 'refresh'
+
+ if (action === 'publish') {
+ const [wf] = await db
+ .select({ isDeployed: workflow.isDeployed })
+ .from(workflow)
+ .where(eq(workflow.id, existingAgent.workflowId))
+ .limit(1)
+
+ if (!wf?.isDeployed) {
+ return NextResponse.json(
+ { error: 'Workflow must be deployed before publishing agent' },
+ { status: 400 }
+ )
+ }
+
+ await db
+ .update(a2aAgent)
+ .set({
+ isPublished: true,
+ publishedAt: new Date(),
+ updatedAt: new Date(),
+ })
+ .where(eq(a2aAgent.id, agentId))
+
+ const redis = getRedisClient()
+ if (redis) {
+ try {
+ await redis.del(`a2a:agent:${agentId}:card`)
+ } catch (err) {
+ logger.warn('Failed to invalidate agent card cache', { agentId, error: err })
+ }
+ }
+
+ logger.info(`Published A2A agent: ${agentId}`)
+ return NextResponse.json({ success: true, isPublished: true })
+ }
+
+ if (action === 'unpublish') {
+ await db
+ .update(a2aAgent)
+ .set({
+ isPublished: false,
+ updatedAt: new Date(),
+ })
+ .where(eq(a2aAgent.id, agentId))
+
+ const redis = getRedisClient()
+ if (redis) {
+ try {
+ await redis.del(`a2a:agent:${agentId}:card`)
+ } catch (err) {
+ logger.warn('Failed to invalidate agent card cache', { agentId, error: err })
+ }
+ }
+
+ logger.info(`Unpublished A2A agent: ${agentId}`)
+ return NextResponse.json({ success: true, isPublished: false })
+ }
+
+ if (action === 'refresh') {
+ const workflowData = await loadWorkflowFromNormalizedTables(existingAgent.workflowId)
+ if (!workflowData) {
+ return NextResponse.json({ error: 'Failed to load workflow' }, { status: 500 })
+ }
+
+ const [wf] = await db
+ .select({ name: workflow.name, description: workflow.description })
+ .from(workflow)
+ .where(eq(workflow.id, existingAgent.workflowId))
+ .limit(1)
+
+ const skills = generateSkillsFromWorkflow(wf?.name || existingAgent.name, wf?.description)
+
+ await db
+ .update(a2aAgent)
+ .set({
+ skills,
+ updatedAt: new Date(),
+ })
+ .where(eq(a2aAgent.id, agentId))
+
+ logger.info(`Refreshed skills for A2A agent: ${agentId}`)
+ return NextResponse.json({ success: true, skills })
+ }
+
+ return NextResponse.json({ error: 'Invalid action' }, { status: 400 })
+ } catch (error) {
+ logger.error('Error with agent action:', error)
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
+ }
+}
diff --git a/apps/sim/app/api/a2a/agents/route.ts b/apps/sim/app/api/a2a/agents/route.ts
new file mode 100644
index 0000000000..e4229ea1e4
--- /dev/null
+++ b/apps/sim/app/api/a2a/agents/route.ts
@@ -0,0 +1,186 @@
+/**
+ * A2A Agents List Endpoint
+ *
+ * List and create A2A agents for a workspace.
+ */
+
+import { db } from '@sim/db'
+import { a2aAgent, workflow } from '@sim/db/schema'
+import { createLogger } from '@sim/logger'
+import { and, eq, sql } from 'drizzle-orm'
+import { type NextRequest, NextResponse } from 'next/server'
+import { v4 as uuidv4 } from 'uuid'
+import { generateSkillsFromWorkflow } from '@/lib/a2a/agent-card'
+import { A2A_DEFAULT_CAPABILITIES } from '@/lib/a2a/constants'
+import { sanitizeAgentName } from '@/lib/a2a/utils'
+import { checkHybridAuth } from '@/lib/auth/hybrid'
+import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils'
+import { hasValidStartBlockInState } from '@/lib/workflows/triggers/trigger-utils'
+import { getWorkspaceById } from '@/lib/workspaces/permissions/utils'
+
+const logger = createLogger('A2AAgentsAPI')
+
+export const dynamic = 'force-dynamic'
+
+/**
+ * GET - List all A2A agents for a workspace
+ */
+export async function GET(request: NextRequest) {
+ try {
+ const auth = await checkHybridAuth(request, { requireWorkflowId: false })
+ if (!auth.success || !auth.userId) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
+ }
+
+ const { searchParams } = new URL(request.url)
+ const workspaceId = searchParams.get('workspaceId')
+
+ if (!workspaceId) {
+ return NextResponse.json({ error: 'workspaceId is required' }, { status: 400 })
+ }
+
+ const ws = await getWorkspaceById(workspaceId)
+ if (!ws) {
+ return NextResponse.json({ error: 'Workspace not found' }, { status: 404 })
+ }
+
+ const agents = await db
+ .select({
+ id: a2aAgent.id,
+ workspaceId: a2aAgent.workspaceId,
+ workflowId: a2aAgent.workflowId,
+ name: a2aAgent.name,
+ description: a2aAgent.description,
+ version: a2aAgent.version,
+ capabilities: a2aAgent.capabilities,
+ skills: a2aAgent.skills,
+ authentication: a2aAgent.authentication,
+ isPublished: a2aAgent.isPublished,
+ publishedAt: a2aAgent.publishedAt,
+ createdAt: a2aAgent.createdAt,
+ updatedAt: a2aAgent.updatedAt,
+ workflowName: workflow.name,
+ workflowDescription: workflow.description,
+ isDeployed: workflow.isDeployed,
+ taskCount: sql`(
+ SELECT COUNT(*)::int
+ FROM "a2a_task"
+ WHERE "a2a_task"."agent_id" = "a2a_agent"."id"
+ )`.as('task_count'),
+ })
+ .from(a2aAgent)
+ .leftJoin(workflow, eq(a2aAgent.workflowId, workflow.id))
+ .where(eq(a2aAgent.workspaceId, workspaceId))
+ .orderBy(a2aAgent.createdAt)
+
+ logger.info(`Listed ${agents.length} A2A agents for workspace ${workspaceId}`)
+
+ return NextResponse.json({ success: true, agents })
+ } catch (error) {
+ logger.error('Error listing agents:', error)
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
+ }
+}
+
+/**
+ * POST - Create a new A2A agent from a workflow
+ */
+export async function POST(request: NextRequest) {
+ try {
+ const auth = await checkHybridAuth(request, { requireWorkflowId: false })
+ if (!auth.success || !auth.userId) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
+ }
+
+ const body = await request.json()
+ const { workspaceId, workflowId, name, description, capabilities, authentication, skillTags } =
+ body
+
+ if (!workspaceId || !workflowId) {
+ return NextResponse.json(
+ { error: 'workspaceId and workflowId are required' },
+ { status: 400 }
+ )
+ }
+
+ const [wf] = await db
+ .select({
+ id: workflow.id,
+ name: workflow.name,
+ description: workflow.description,
+ workspaceId: workflow.workspaceId,
+ isDeployed: workflow.isDeployed,
+ })
+ .from(workflow)
+ .where(and(eq(workflow.id, workflowId), eq(workflow.workspaceId, workspaceId)))
+ .limit(1)
+
+ if (!wf) {
+ return NextResponse.json(
+ { error: 'Workflow not found or does not belong to workspace' },
+ { status: 404 }
+ )
+ }
+
+ const workflowData = await loadWorkflowFromNormalizedTables(workflowId)
+ if (!workflowData || !hasValidStartBlockInState(workflowData)) {
+ return NextResponse.json(
+ { error: 'Workflow must have a Start block to be exposed as an A2A agent' },
+ { status: 400 }
+ )
+ }
+
+ const [existing] = await db
+ .select({ id: a2aAgent.id })
+ .from(a2aAgent)
+ .where(and(eq(a2aAgent.workspaceId, workspaceId), eq(a2aAgent.workflowId, workflowId)))
+ .limit(1)
+
+ if (existing) {
+ return NextResponse.json(
+ { error: 'An agent already exists for this workflow' },
+ { status: 409 }
+ )
+ }
+
+ const skills = generateSkillsFromWorkflow(
+ name || wf.name,
+ description || wf.description,
+ skillTags
+ )
+
+ const agentId = uuidv4()
+ const agentName = name || sanitizeAgentName(wf.name)
+
+ const [agent] = await db
+ .insert(a2aAgent)
+ .values({
+ id: agentId,
+ workspaceId,
+ workflowId,
+ createdBy: auth.userId,
+ name: agentName,
+ description: description || wf.description,
+ version: '1.0.0',
+ capabilities: {
+ ...A2A_DEFAULT_CAPABILITIES,
+ ...capabilities,
+ },
+ skills,
+ authentication: authentication || {
+ schemes: ['bearer', 'apiKey'],
+ },
+ isPublished: false,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ })
+ .returning()
+
+ logger.info(`Created A2A agent ${agentId} for workflow ${workflowId}`)
+
+ return NextResponse.json({ success: true, agent }, { status: 201 })
+ } catch (error) {
+ logger.error('Error creating agent:', error)
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
+ }
+}
diff --git a/apps/sim/app/api/a2a/serve/[agentId]/route.ts b/apps/sim/app/api/a2a/serve/[agentId]/route.ts
new file mode 100644
index 0000000000..aed8c3c205
--- /dev/null
+++ b/apps/sim/app/api/a2a/serve/[agentId]/route.ts
@@ -0,0 +1,1263 @@
+import type { Artifact, Message, PushNotificationConfig, TaskState } from '@a2a-js/sdk'
+import { db } from '@sim/db'
+import { a2aAgent, a2aPushNotificationConfig, a2aTask, workflow } from '@sim/db/schema'
+import { createLogger } from '@sim/logger'
+import { eq } from 'drizzle-orm'
+import { type NextRequest, NextResponse } from 'next/server'
+import { v4 as uuidv4 } from 'uuid'
+import { A2A_DEFAULT_TIMEOUT, A2A_MAX_HISTORY_LENGTH } from '@/lib/a2a/constants'
+import { notifyTaskStateChange } from '@/lib/a2a/push-notifications'
+import {
+ createAgentMessage,
+ extractWorkflowInput,
+ isTerminalState,
+ parseWorkflowSSEChunk,
+} from '@/lib/a2a/utils'
+import { checkHybridAuth } from '@/lib/auth/hybrid'
+import { getBrandConfig } from '@/lib/branding/branding'
+import { acquireLock, getRedisClient, releaseLock } from '@/lib/core/config/redis'
+import { SSE_HEADERS } from '@/lib/core/utils/sse'
+import { getBaseUrl } from '@/lib/core/utils/urls'
+import { markExecutionCancelled } from '@/lib/execution/cancellation'
+import {
+ A2A_ERROR_CODES,
+ A2A_METHODS,
+ buildExecuteRequest,
+ buildTaskResponse,
+ createError,
+ createResponse,
+ extractAgentContent,
+ formatTaskResponse,
+ generateTaskId,
+ isJSONRPCRequest,
+ type MessageSendParams,
+ type PushNotificationSetParams,
+ type TaskIdParams,
+} from '@/app/api/a2a/serve/[agentId]/utils'
+
+const logger = createLogger('A2AServeAPI')
+
+export const dynamic = 'force-dynamic'
+export const runtime = 'nodejs'
+
+interface RouteParams {
+ agentId: string
+}
+
+/**
+ * GET - Returns the Agent Card (discovery document)
+ */
+export async function GET(_request: NextRequest, { params }: { params: Promise }) {
+ const { agentId } = await params
+
+ const redis = getRedisClient()
+ const cacheKey = `a2a:agent:${agentId}:card`
+
+ if (redis) {
+ try {
+ const cached = await redis.get(cacheKey)
+ if (cached) {
+ return NextResponse.json(JSON.parse(cached), {
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Cache-Control': 'private, max-age=60',
+ 'X-Cache': 'HIT',
+ },
+ })
+ }
+ } catch (err) {
+ logger.warn('Redis cache read failed', { agentId, error: err })
+ }
+ }
+
+ try {
+ const [agent] = await db
+ .select({
+ id: a2aAgent.id,
+ name: a2aAgent.name,
+ description: a2aAgent.description,
+ version: a2aAgent.version,
+ capabilities: a2aAgent.capabilities,
+ skills: a2aAgent.skills,
+ authentication: a2aAgent.authentication,
+ isPublished: a2aAgent.isPublished,
+ })
+ .from(a2aAgent)
+ .where(eq(a2aAgent.id, agentId))
+ .limit(1)
+
+ if (!agent) {
+ return NextResponse.json({ error: 'Agent not found' }, { status: 404 })
+ }
+
+ if (!agent.isPublished) {
+ return NextResponse.json({ error: 'Agent not published' }, { status: 404 })
+ }
+
+ const baseUrl = getBaseUrl()
+ const brandConfig = getBrandConfig()
+
+ const authConfig = agent.authentication as { schemes?: string[] } | undefined
+ const schemes = authConfig?.schemes || []
+ const isPublic = schemes.includes('none')
+
+ const agentCard = {
+ protocolVersion: '0.3.0',
+ name: agent.name,
+ description: agent.description || '',
+ url: `${baseUrl}/api/a2a/serve/${agent.id}`,
+ version: agent.version,
+ preferredTransport: 'JSONRPC',
+ documentationUrl: `${baseUrl}/docs/a2a`,
+ provider: {
+ organization: brandConfig.name,
+ url: baseUrl,
+ },
+ capabilities: agent.capabilities,
+ skills: agent.skills || [],
+ ...(isPublic
+ ? {}
+ : {
+ securitySchemes: {
+ apiKey: {
+ type: 'apiKey' as const,
+ name: 'X-API-Key',
+ in: 'header' as const,
+ description: 'API key authentication',
+ },
+ },
+ security: [{ apiKey: [] }],
+ }),
+ defaultInputModes: ['text/plain', 'application/json'],
+ defaultOutputModes: ['text/plain', 'application/json'],
+ }
+
+ if (redis) {
+ try {
+ await redis.set(cacheKey, JSON.stringify(agentCard), 'EX', 60)
+ } catch (err) {
+ logger.warn('Redis cache write failed', { agentId, error: err })
+ }
+ }
+
+ return NextResponse.json(agentCard, {
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Cache-Control': 'private, max-age=60',
+ 'X-Cache': 'MISS',
+ },
+ })
+ } catch (error) {
+ logger.error('Error getting Agent Card:', error)
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
+ }
+}
+
+/**
+ * POST - Handle JSON-RPC requests
+ */
+export async function POST(request: NextRequest, { params }: { params: Promise }) {
+ const { agentId } = await params
+
+ try {
+ const [agent] = await db
+ .select({
+ id: a2aAgent.id,
+ name: a2aAgent.name,
+ workflowId: a2aAgent.workflowId,
+ workspaceId: a2aAgent.workspaceId,
+ isPublished: a2aAgent.isPublished,
+ capabilities: a2aAgent.capabilities,
+ authentication: a2aAgent.authentication,
+ })
+ .from(a2aAgent)
+ .where(eq(a2aAgent.id, agentId))
+ .limit(1)
+
+ if (!agent) {
+ return NextResponse.json(
+ createError(null, A2A_ERROR_CODES.AGENT_UNAVAILABLE, 'Agent not found'),
+ { status: 404 }
+ )
+ }
+
+ if (!agent.isPublished) {
+ return NextResponse.json(
+ createError(null, A2A_ERROR_CODES.AGENT_UNAVAILABLE, 'Agent not published'),
+ { status: 404 }
+ )
+ }
+
+ const authSchemes = (agent.authentication as { schemes?: string[] })?.schemes || []
+ const requiresAuth = !authSchemes.includes('none')
+
+ if (requiresAuth) {
+ const auth = await checkHybridAuth(request, { requireWorkflowId: false })
+ if (!auth.success || !auth.userId) {
+ return NextResponse.json(
+ createError(null, A2A_ERROR_CODES.AUTHENTICATION_REQUIRED, 'Unauthorized'),
+ { status: 401 }
+ )
+ }
+ }
+
+ const [wf] = await db
+ .select({ isDeployed: workflow.isDeployed })
+ .from(workflow)
+ .where(eq(workflow.id, agent.workflowId))
+ .limit(1)
+
+ if (!wf?.isDeployed) {
+ return NextResponse.json(
+ createError(null, A2A_ERROR_CODES.AGENT_UNAVAILABLE, 'Workflow is not deployed'),
+ { status: 400 }
+ )
+ }
+
+ const body = await request.json()
+
+ if (!isJSONRPCRequest(body)) {
+ return NextResponse.json(
+ createError(null, A2A_ERROR_CODES.INVALID_REQUEST, 'Invalid JSON-RPC request'),
+ { status: 400 }
+ )
+ }
+
+ const { id, method, params: rpcParams } = body
+ const apiKey = request.headers.get('X-API-Key')
+
+ logger.info(`A2A request: ${method} for agent ${agentId}`)
+
+ switch (method) {
+ case A2A_METHODS.MESSAGE_SEND:
+ return handleMessageSend(id, agent, rpcParams as MessageSendParams, apiKey)
+
+ case A2A_METHODS.MESSAGE_STREAM:
+ return handleMessageStream(request, id, agent, rpcParams as MessageSendParams, apiKey)
+
+ case A2A_METHODS.TASKS_GET:
+ return handleTaskGet(id, rpcParams as TaskIdParams)
+
+ case A2A_METHODS.TASKS_CANCEL:
+ return handleTaskCancel(id, rpcParams as TaskIdParams)
+
+ case A2A_METHODS.TASKS_RESUBSCRIBE:
+ return handleTaskResubscribe(request, id, rpcParams as TaskIdParams)
+
+ case A2A_METHODS.PUSH_NOTIFICATION_SET:
+ return handlePushNotificationSet(id, rpcParams as PushNotificationSetParams)
+
+ case A2A_METHODS.PUSH_NOTIFICATION_GET:
+ return handlePushNotificationGet(id, rpcParams as TaskIdParams)
+
+ case A2A_METHODS.PUSH_NOTIFICATION_DELETE:
+ return handlePushNotificationDelete(id, rpcParams as TaskIdParams)
+
+ default:
+ return NextResponse.json(
+ createError(id, A2A_ERROR_CODES.METHOD_NOT_FOUND, `Method not found: ${method}`),
+ { status: 404 }
+ )
+ }
+ } catch (error) {
+ logger.error('Error handling A2A request:', error)
+ return NextResponse.json(createError(null, A2A_ERROR_CODES.INTERNAL_ERROR, 'Internal error'), {
+ status: 500,
+ })
+ }
+}
+
+/**
+ * Handle message/send - Send a message (v0.3)
+ */
+async function handleMessageSend(
+ id: string | number,
+ agent: {
+ id: string
+ name: string
+ workflowId: string
+ workspaceId: string
+ },
+ params: MessageSendParams,
+ apiKey?: string | null
+): Promise {
+ if (!params?.message) {
+ return NextResponse.json(
+ createError(id, A2A_ERROR_CODES.INVALID_PARAMS, 'Message is required'),
+ { status: 400 }
+ )
+ }
+
+ const message = params.message
+ const taskId = message.taskId || generateTaskId()
+ const contextId = message.contextId || uuidv4()
+
+ // Distributed lock to prevent concurrent task processing
+ const lockKey = `a2a:task:${taskId}:lock`
+ const lockValue = uuidv4()
+ const acquired = await acquireLock(lockKey, lockValue, 60)
+
+ if (!acquired) {
+ return NextResponse.json(
+ createError(id, A2A_ERROR_CODES.INTERNAL_ERROR, 'Task is currently being processed'),
+ { status: 409 }
+ )
+ }
+
+ try {
+ let existingTask: typeof a2aTask.$inferSelect | null = null
+ if (message.taskId) {
+ const [found] = await db.select().from(a2aTask).where(eq(a2aTask.id, message.taskId)).limit(1)
+ existingTask = found || null
+
+ if (!existingTask) {
+ return NextResponse.json(
+ createError(id, A2A_ERROR_CODES.TASK_NOT_FOUND, 'Task not found'),
+ { status: 404 }
+ )
+ }
+
+ if (isTerminalState(existingTask.status as TaskState)) {
+ return NextResponse.json(
+ createError(id, A2A_ERROR_CODES.TASK_ALREADY_COMPLETE, 'Task already in terminal state'),
+ { status: 400 }
+ )
+ }
+ }
+
+ const history: Message[] = existingTask?.messages ? (existingTask.messages as Message[]) : []
+
+ history.push(message)
+
+ if (history.length > A2A_MAX_HISTORY_LENGTH) {
+ history.splice(0, history.length - A2A_MAX_HISTORY_LENGTH)
+ }
+
+ if (existingTask) {
+ await db
+ .update(a2aTask)
+ .set({
+ status: 'working',
+ messages: history,
+ updatedAt: new Date(),
+ })
+ .where(eq(a2aTask.id, taskId))
+ } else {
+ await db.insert(a2aTask).values({
+ id: taskId,
+ agentId: agent.id,
+ sessionId: contextId || null,
+ status: 'working',
+ messages: history,
+ metadata: {},
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ })
+ }
+
+ const {
+ url: executeUrl,
+ headers,
+ useInternalAuth,
+ } = await buildExecuteRequest({
+ workflowId: agent.workflowId,
+ apiKey,
+ })
+
+ logger.info(`Executing workflow ${agent.workflowId} for A2A task ${taskId}`)
+
+ try {
+ const workflowInput = extractWorkflowInput(message)
+ if (!workflowInput) {
+ return NextResponse.json(
+ createError(
+ id,
+ A2A_ERROR_CODES.INVALID_PARAMS,
+ 'Message must contain at least one part with content'
+ ),
+ { status: 400 }
+ )
+ }
+
+ const response = await fetch(executeUrl, {
+ method: 'POST',
+ headers,
+ body: JSON.stringify({
+ ...workflowInput,
+ triggerType: 'a2a',
+ ...(useInternalAuth && { workflowId: agent.workflowId }),
+ }),
+ signal: AbortSignal.timeout(A2A_DEFAULT_TIMEOUT),
+ })
+
+ const executeResult = await response.json()
+
+ const finalState: TaskState = response.ok ? 'completed' : 'failed'
+
+ const agentContent = extractAgentContent(executeResult)
+ const agentMessage = createAgentMessage(agentContent)
+ agentMessage.taskId = taskId
+ if (contextId) agentMessage.contextId = contextId
+ history.push(agentMessage)
+
+ const artifacts = executeResult.output?.artifacts || []
+
+ await db
+ .update(a2aTask)
+ .set({
+ status: finalState,
+ messages: history,
+ artifacts,
+ executionId: executeResult.metadata?.executionId,
+ completedAt: new Date(),
+ updatedAt: new Date(),
+ })
+ .where(eq(a2aTask.id, taskId))
+
+ if (isTerminalState(finalState)) {
+ notifyTaskStateChange(taskId, finalState).catch((err) => {
+ logger.error('Failed to trigger push notification', { taskId, error: err })
+ })
+ }
+
+ const task = buildTaskResponse({
+ taskId,
+ contextId,
+ state: finalState,
+ history,
+ artifacts,
+ })
+
+ return NextResponse.json(createResponse(id, task))
+ } catch (error) {
+ const isTimeout = error instanceof Error && error.name === 'TimeoutError'
+ logger.error(`Error executing workflow for task ${taskId}:`, { error, isTimeout })
+
+ const errorMessage = isTimeout
+ ? `Workflow execution timed out after ${A2A_DEFAULT_TIMEOUT}ms`
+ : error instanceof Error
+ ? error.message
+ : 'Workflow execution failed'
+
+ await db
+ .update(a2aTask)
+ .set({
+ status: 'failed',
+ updatedAt: new Date(),
+ completedAt: new Date(),
+ })
+ .where(eq(a2aTask.id, taskId))
+
+ notifyTaskStateChange(taskId, 'failed').catch((err) => {
+ logger.error('Failed to trigger push notification for failure', { taskId, error: err })
+ })
+
+ return NextResponse.json(createError(id, A2A_ERROR_CODES.INTERNAL_ERROR, errorMessage), {
+ status: 500,
+ })
+ }
+ } finally {
+ await releaseLock(lockKey, lockValue)
+ }
+}
+
+/**
+ * Handle message/stream - Stream a message response (v0.3)
+ */
+async function handleMessageStream(
+ _request: NextRequest,
+ id: string | number,
+ agent: {
+ id: string
+ name: string
+ workflowId: string
+ workspaceId: string
+ },
+ params: MessageSendParams,
+ apiKey?: string | null
+): Promise {
+ if (!params?.message) {
+ return NextResponse.json(
+ createError(id, A2A_ERROR_CODES.INVALID_PARAMS, 'Message is required'),
+ { status: 400 }
+ )
+ }
+
+ const message = params.message
+ const contextId = message.contextId || uuidv4()
+ const taskId = message.taskId || generateTaskId()
+
+ // Distributed lock to prevent concurrent task processing
+ const lockKey = `a2a:task:${taskId}:lock`
+ const lockValue = uuidv4()
+ const acquired = await acquireLock(lockKey, lockValue, 300)
+
+ if (!acquired) {
+ const encoder = new TextEncoder()
+ const errorStream = new ReadableStream({
+ start(controller) {
+ controller.enqueue(
+ encoder.encode(
+ `event: error\ndata: ${JSON.stringify({ code: A2A_ERROR_CODES.INTERNAL_ERROR, message: 'Task is currently being processed' })}\n\n`
+ )
+ )
+ controller.close()
+ },
+ })
+ return new NextResponse(errorStream, { headers: SSE_HEADERS })
+ }
+
+ let history: Message[] = []
+ let existingTask: typeof a2aTask.$inferSelect | null = null
+
+ if (message.taskId) {
+ const [found] = await db.select().from(a2aTask).where(eq(a2aTask.id, message.taskId)).limit(1)
+ existingTask = found || null
+
+ if (!existingTask) {
+ await releaseLock(lockKey, lockValue)
+ return NextResponse.json(createError(id, A2A_ERROR_CODES.TASK_NOT_FOUND, 'Task not found'), {
+ status: 404,
+ })
+ }
+
+ if (isTerminalState(existingTask.status as TaskState)) {
+ await releaseLock(lockKey, lockValue)
+ return NextResponse.json(
+ createError(id, A2A_ERROR_CODES.TASK_ALREADY_COMPLETE, 'Task already in terminal state'),
+ { status: 400 }
+ )
+ }
+
+ history = existingTask.messages as Message[]
+ }
+
+ history.push(message)
+
+ if (history.length > A2A_MAX_HISTORY_LENGTH) {
+ history.splice(0, history.length - A2A_MAX_HISTORY_LENGTH)
+ }
+
+ if (existingTask) {
+ await db
+ .update(a2aTask)
+ .set({
+ status: 'working',
+ messages: history,
+ updatedAt: new Date(),
+ })
+ .where(eq(a2aTask.id, taskId))
+ } else {
+ await db.insert(a2aTask).values({
+ id: taskId,
+ agentId: agent.id,
+ sessionId: contextId || null,
+ status: 'working',
+ messages: history,
+ metadata: {},
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ })
+ }
+
+ const encoder = new TextEncoder()
+
+ const stream = new ReadableStream({
+ async start(controller) {
+ const sendEvent = (event: string, data: unknown) => {
+ try {
+ const jsonRpcResponse = {
+ jsonrpc: '2.0' as const,
+ id,
+ result: data,
+ }
+ controller.enqueue(
+ encoder.encode(`event: ${event}\ndata: ${JSON.stringify(jsonRpcResponse)}\n\n`)
+ )
+ } catch (error) {
+ logger.error('Error sending SSE event:', error)
+ }
+ }
+
+ sendEvent('status', {
+ kind: 'status',
+ taskId,
+ contextId,
+ status: { state: 'working', timestamp: new Date().toISOString() },
+ })
+
+ try {
+ const {
+ url: executeUrl,
+ headers,
+ useInternalAuth,
+ } = await buildExecuteRequest({
+ workflowId: agent.workflowId,
+ apiKey,
+ stream: true,
+ })
+
+ const workflowInput = extractWorkflowInput(message)
+ if (!workflowInput) {
+ sendEvent('error', {
+ code: A2A_ERROR_CODES.INVALID_PARAMS,
+ message: 'Message must contain at least one part with content',
+ })
+ await releaseLock(lockKey, lockValue)
+ controller.close()
+ return
+ }
+
+ const response = await fetch(executeUrl, {
+ method: 'POST',
+ headers,
+ body: JSON.stringify({
+ ...workflowInput,
+ triggerType: 'a2a',
+ stream: true,
+ ...(useInternalAuth && { workflowId: agent.workflowId }),
+ }),
+ signal: AbortSignal.timeout(A2A_DEFAULT_TIMEOUT),
+ })
+
+ if (!response.ok) {
+ let errorMessage = 'Workflow execution failed'
+ try {
+ const errorResult = await response.json()
+ errorMessage = errorResult.error || errorMessage
+ } catch {
+ // Response may not be JSON
+ }
+ throw new Error(errorMessage)
+ }
+
+ const contentType = response.headers.get('content-type') || ''
+ const isStreamingResponse =
+ contentType.includes('text/event-stream') || contentType.includes('text/plain')
+
+ if (response.body && isStreamingResponse) {
+ const reader = response.body.getReader()
+ const decoder = new TextDecoder()
+ let accumulatedContent = ''
+ let finalContent: string | undefined
+
+ while (true) {
+ const { done, value } = await reader.read()
+ if (done) break
+
+ const rawChunk = decoder.decode(value, { stream: true })
+ const parsed = parseWorkflowSSEChunk(rawChunk)
+
+ if (parsed.content) {
+ accumulatedContent += parsed.content
+ sendEvent('message', {
+ kind: 'message',
+ taskId,
+ contextId,
+ role: 'agent',
+ parts: [{ kind: 'text', text: parsed.content }],
+ final: false,
+ })
+ }
+
+ if (parsed.finalContent) {
+ finalContent = parsed.finalContent
+ }
+ }
+
+ const messageContent =
+ (finalContent !== undefined && finalContent.length > 0
+ ? finalContent
+ : accumulatedContent) || 'Task completed'
+ const agentMessage = createAgentMessage(messageContent)
+ agentMessage.taskId = taskId
+ if (contextId) agentMessage.contextId = contextId
+ history.push(agentMessage)
+
+ await db
+ .update(a2aTask)
+ .set({
+ status: 'completed',
+ messages: history,
+ completedAt: new Date(),
+ updatedAt: new Date(),
+ })
+ .where(eq(a2aTask.id, taskId))
+
+ notifyTaskStateChange(taskId, 'completed').catch((err) => {
+ logger.error('Failed to trigger push notification', { taskId, error: err })
+ })
+
+ sendEvent('task', {
+ kind: 'task',
+ id: taskId,
+ contextId,
+ status: { state: 'completed', timestamp: new Date().toISOString() },
+ history,
+ artifacts: [],
+ })
+ } else {
+ const result = await response.json()
+
+ const content = extractAgentContent(result)
+
+ sendEvent('message', {
+ kind: 'message',
+ taskId,
+ contextId,
+ role: 'agent',
+ parts: [{ kind: 'text', text: content }],
+ final: true,
+ })
+
+ const agentMessage = createAgentMessage(content)
+ agentMessage.taskId = taskId
+ if (contextId) agentMessage.contextId = contextId
+ history.push(agentMessage)
+
+ const artifacts = (result.output?.artifacts as Artifact[]) || []
+
+ await db
+ .update(a2aTask)
+ .set({
+ status: 'completed',
+ messages: history,
+ artifacts,
+ executionId: result.metadata?.executionId,
+ completedAt: new Date(),
+ updatedAt: new Date(),
+ })
+ .where(eq(a2aTask.id, taskId))
+
+ notifyTaskStateChange(taskId, 'completed').catch((err) => {
+ logger.error('Failed to trigger push notification', { taskId, error: err })
+ })
+
+ sendEvent('task', {
+ kind: 'task',
+ id: taskId,
+ contextId,
+ status: { state: 'completed', timestamp: new Date().toISOString() },
+ history,
+ artifacts,
+ })
+ }
+ } catch (error) {
+ const isTimeout = error instanceof Error && error.name === 'TimeoutError'
+ logger.error(`Streaming error for task ${taskId}:`, { error, isTimeout })
+
+ const errorMessage = isTimeout
+ ? `Workflow execution timed out after ${A2A_DEFAULT_TIMEOUT}ms`
+ : error instanceof Error
+ ? error.message
+ : 'Streaming failed'
+
+ await db
+ .update(a2aTask)
+ .set({
+ status: 'failed',
+ completedAt: new Date(),
+ updatedAt: new Date(),
+ })
+ .where(eq(a2aTask.id, taskId))
+
+ notifyTaskStateChange(taskId, 'failed').catch((err) => {
+ logger.error('Failed to trigger push notification for failure', { taskId, error: err })
+ })
+
+ sendEvent('error', {
+ code: A2A_ERROR_CODES.INTERNAL_ERROR,
+ message: errorMessage,
+ })
+ } finally {
+ await releaseLock(lockKey, lockValue)
+ controller.close()
+ }
+ },
+ })
+
+ return new NextResponse(stream, {
+ headers: {
+ ...SSE_HEADERS,
+ 'X-Task-Id': taskId,
+ },
+ })
+}
+
+/**
+ * Handle tasks/get - Query task status
+ */
+async function handleTaskGet(id: string | number, params: TaskIdParams): Promise {
+ if (!params?.id) {
+ return NextResponse.json(
+ createError(id, A2A_ERROR_CODES.INVALID_PARAMS, 'Task ID is required'),
+ { status: 400 }
+ )
+ }
+
+ const historyLength =
+ params.historyLength !== undefined && params.historyLength >= 0
+ ? params.historyLength
+ : undefined
+
+ const [task] = await db.select().from(a2aTask).where(eq(a2aTask.id, params.id)).limit(1)
+
+ if (!task) {
+ return NextResponse.json(createError(id, A2A_ERROR_CODES.TASK_NOT_FOUND, 'Task not found'), {
+ status: 404,
+ })
+ }
+
+ const taskResponse = buildTaskResponse({
+ taskId: task.id,
+ contextId: task.sessionId || task.id,
+ state: task.status as TaskState,
+ history: task.messages as Message[],
+ artifacts: (task.artifacts as Artifact[]) || [],
+ })
+
+ const result = formatTaskResponse(taskResponse, historyLength)
+
+ return NextResponse.json(createResponse(id, result))
+}
+
+/**
+ * Handle tasks/cancel - Cancel a running task
+ */
+async function handleTaskCancel(id: string | number, params: TaskIdParams): Promise {
+ if (!params?.id) {
+ return NextResponse.json(
+ createError(id, A2A_ERROR_CODES.INVALID_PARAMS, 'Task ID is required'),
+ { status: 400 }
+ )
+ }
+
+ const [task] = await db.select().from(a2aTask).where(eq(a2aTask.id, params.id)).limit(1)
+
+ if (!task) {
+ return NextResponse.json(createError(id, A2A_ERROR_CODES.TASK_NOT_FOUND, 'Task not found'), {
+ status: 404,
+ })
+ }
+
+ if (isTerminalState(task.status as TaskState)) {
+ return NextResponse.json(
+ createError(id, A2A_ERROR_CODES.TASK_ALREADY_COMPLETE, 'Task already in terminal state'),
+ { status: 400 }
+ )
+ }
+
+ if (task.executionId) {
+ try {
+ await markExecutionCancelled(task.executionId)
+ logger.info('Cancelled workflow execution', {
+ taskId: task.id,
+ executionId: task.executionId,
+ })
+ } catch (error) {
+ logger.warn('Failed to cancel workflow execution', {
+ taskId: task.id,
+ executionId: task.executionId,
+ error,
+ })
+ }
+ }
+
+ await db
+ .update(a2aTask)
+ .set({
+ status: 'canceled',
+ updatedAt: new Date(),
+ completedAt: new Date(),
+ })
+ .where(eq(a2aTask.id, params.id))
+
+ notifyTaskStateChange(params.id, 'canceled').catch((err) => {
+ logger.error('Failed to trigger push notification for cancellation', {
+ taskId: params.id,
+ error: err,
+ })
+ })
+
+ const canceledTask = buildTaskResponse({
+ taskId: task.id,
+ contextId: task.sessionId || task.id,
+ state: 'canceled',
+ history: task.messages as Message[],
+ artifacts: (task.artifacts as Artifact[]) || [],
+ })
+
+ return NextResponse.json(createResponse(id, canceledTask))
+}
+
+/**
+ * Handle tasks/resubscribe - Reconnect to SSE stream for an ongoing task
+ */
+async function handleTaskResubscribe(
+ request: NextRequest,
+ id: string | number,
+ params: TaskIdParams
+): Promise {
+ if (!params?.id) {
+ return NextResponse.json(
+ createError(id, A2A_ERROR_CODES.INVALID_PARAMS, 'Task ID is required'),
+ { status: 400 }
+ )
+ }
+
+ const [task] = await db.select().from(a2aTask).where(eq(a2aTask.id, params.id)).limit(1)
+
+ if (!task) {
+ return NextResponse.json(createError(id, A2A_ERROR_CODES.TASK_NOT_FOUND, 'Task not found'), {
+ status: 404,
+ })
+ }
+
+ const encoder = new TextEncoder()
+
+ if (isTerminalState(task.status as TaskState)) {
+ const completedTask = buildTaskResponse({
+ taskId: task.id,
+ contextId: task.sessionId || task.id,
+ state: task.status as TaskState,
+ history: task.messages as Message[],
+ artifacts: (task.artifacts as Artifact[]) || [],
+ })
+ const jsonRpcResponse = { jsonrpc: '2.0' as const, id, result: completedTask }
+ const sseData = `event: task\ndata: ${JSON.stringify(jsonRpcResponse)}\n\n`
+ const stream = new ReadableStream({
+ start(controller) {
+ controller.enqueue(encoder.encode(sseData))
+ controller.close()
+ },
+ })
+ return new NextResponse(stream, { headers: SSE_HEADERS })
+ }
+ let isCancelled = false
+ let pollTimeoutId: ReturnType | null = null
+
+ const abortSignal = request.signal
+ abortSignal.addEventListener('abort', () => {
+ isCancelled = true
+ if (pollTimeoutId) {
+ clearTimeout(pollTimeoutId)
+ pollTimeoutId = null
+ }
+ })
+
+ const stream = new ReadableStream({
+ async start(controller) {
+ const sendEvent = (event: string, data: unknown): boolean => {
+ if (isCancelled || abortSignal.aborted) return false
+ try {
+ const jsonRpcResponse = { jsonrpc: '2.0' as const, id, result: data }
+ controller.enqueue(
+ encoder.encode(`event: ${event}\ndata: ${JSON.stringify(jsonRpcResponse)}\n\n`)
+ )
+ return true
+ } catch (error) {
+ logger.error('Error sending SSE event:', error)
+ isCancelled = true
+ return false
+ }
+ }
+
+ const cleanup = () => {
+ isCancelled = true
+ if (pollTimeoutId) {
+ clearTimeout(pollTimeoutId)
+ pollTimeoutId = null
+ }
+ }
+
+ if (
+ !sendEvent('status', {
+ kind: 'status',
+ taskId: task.id,
+ contextId: task.sessionId,
+ status: { state: task.status, timestamp: new Date().toISOString() },
+ })
+ ) {
+ cleanup()
+ return
+ }
+
+ const pollInterval = 3000 // 3 seconds
+ const maxPolls = 100 // 5 minutes max
+
+ let polls = 0
+ const poll = async () => {
+ if (isCancelled || abortSignal.aborted) {
+ cleanup()
+ return
+ }
+
+ polls++
+ if (polls > maxPolls) {
+ cleanup()
+ try {
+ controller.close()
+ } catch {
+ // Already closed
+ }
+ return
+ }
+
+ try {
+ const [updatedTask] = await db
+ .select()
+ .from(a2aTask)
+ .where(eq(a2aTask.id, params.id))
+ .limit(1)
+
+ if (isCancelled) {
+ cleanup()
+ return
+ }
+
+ if (!updatedTask) {
+ sendEvent('error', { code: A2A_ERROR_CODES.TASK_NOT_FOUND, message: 'Task not found' })
+ cleanup()
+ try {
+ controller.close()
+ } catch {
+ // Already closed
+ }
+ return
+ }
+
+ if (updatedTask.status !== task.status) {
+ if (
+ !sendEvent('status', {
+ kind: 'status',
+ taskId: updatedTask.id,
+ contextId: updatedTask.sessionId,
+ status: { state: updatedTask.status, timestamp: new Date().toISOString() },
+ final: isTerminalState(updatedTask.status as TaskState),
+ })
+ ) {
+ cleanup()
+ return
+ }
+ }
+
+ if (isTerminalState(updatedTask.status as TaskState)) {
+ const messages = updatedTask.messages as Message[]
+ const lastMessage = messages[messages.length - 1]
+ if (lastMessage && lastMessage.role === 'agent') {
+ sendEvent('message', {
+ ...lastMessage,
+ taskId: updatedTask.id,
+ contextId: updatedTask.sessionId || updatedTask.id,
+ final: true,
+ })
+ }
+
+ cleanup()
+ try {
+ controller.close()
+ } catch {
+ // Already closed
+ }
+ return
+ }
+
+ pollTimeoutId = setTimeout(poll, pollInterval)
+ } catch (error) {
+ logger.error('Error during SSE poll:', error)
+ sendEvent('error', {
+ code: A2A_ERROR_CODES.INTERNAL_ERROR,
+ message: error instanceof Error ? error.message : 'Polling failed',
+ })
+ cleanup()
+ try {
+ controller.close()
+ } catch {
+ // Already closed
+ }
+ }
+ }
+
+ poll()
+ },
+ cancel() {
+ isCancelled = true
+ if (pollTimeoutId) {
+ clearTimeout(pollTimeoutId)
+ pollTimeoutId = null
+ }
+ },
+ })
+
+ return new NextResponse(stream, {
+ headers: {
+ ...SSE_HEADERS,
+ 'X-Task-Id': params.id,
+ },
+ })
+}
+
+/**
+ * Handle tasks/pushNotificationConfig/set - Set webhook for task updates
+ */
+async function handlePushNotificationSet(
+ id: string | number,
+ params: PushNotificationSetParams
+): Promise {
+ if (!params?.id) {
+ return NextResponse.json(
+ createError(id, A2A_ERROR_CODES.INVALID_PARAMS, 'Task ID is required'),
+ { status: 400 }
+ )
+ }
+
+ if (!params?.pushNotificationConfig?.url) {
+ return NextResponse.json(
+ createError(id, A2A_ERROR_CODES.INVALID_PARAMS, 'Push notification URL is required'),
+ { status: 400 }
+ )
+ }
+
+ try {
+ const url = new URL(params.pushNotificationConfig.url)
+ if (url.protocol !== 'https:') {
+ return NextResponse.json(
+ createError(id, A2A_ERROR_CODES.INVALID_PARAMS, 'Push notification URL must use HTTPS'),
+ { status: 400 }
+ )
+ }
+ } catch {
+ return NextResponse.json(
+ createError(id, A2A_ERROR_CODES.INVALID_PARAMS, 'Invalid push notification URL'),
+ { status: 400 }
+ )
+ }
+
+ const [task] = await db.select().from(a2aTask).where(eq(a2aTask.id, params.id)).limit(1)
+
+ if (!task) {
+ return NextResponse.json(createError(id, A2A_ERROR_CODES.TASK_NOT_FOUND, 'Task not found'), {
+ status: 404,
+ })
+ }
+
+ const [existingConfig] = await db
+ .select()
+ .from(a2aPushNotificationConfig)
+ .where(eq(a2aPushNotificationConfig.taskId, params.id))
+ .limit(1)
+
+ const config = params.pushNotificationConfig
+
+ if (existingConfig) {
+ await db
+ .update(a2aPushNotificationConfig)
+ .set({
+ url: config.url,
+ token: config.token || null,
+ isActive: true,
+ updatedAt: new Date(),
+ })
+ .where(eq(a2aPushNotificationConfig.id, existingConfig.id))
+ } else {
+ await db.insert(a2aPushNotificationConfig).values({
+ id: uuidv4(),
+ taskId: params.id,
+ url: config.url,
+ token: config.token || null,
+ isActive: true,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ })
+ }
+
+ const result: PushNotificationConfig = {
+ url: config.url,
+ token: config.token,
+ }
+
+ return NextResponse.json(createResponse(id, result))
+}
+
+/**
+ * Handle tasks/pushNotificationConfig/get - Get webhook config for a task
+ */
+async function handlePushNotificationGet(
+ id: string | number,
+ params: TaskIdParams
+): Promise {
+ if (!params?.id) {
+ return NextResponse.json(
+ createError(id, A2A_ERROR_CODES.INVALID_PARAMS, 'Task ID is required'),
+ { status: 400 }
+ )
+ }
+
+ const [task] = await db.select().from(a2aTask).where(eq(a2aTask.id, params.id)).limit(1)
+
+ if (!task) {
+ return NextResponse.json(createError(id, A2A_ERROR_CODES.TASK_NOT_FOUND, 'Task not found'), {
+ status: 404,
+ })
+ }
+
+ const [config] = await db
+ .select()
+ .from(a2aPushNotificationConfig)
+ .where(eq(a2aPushNotificationConfig.taskId, params.id))
+ .limit(1)
+
+ if (!config) {
+ return NextResponse.json(
+ createError(id, A2A_ERROR_CODES.TASK_NOT_FOUND, 'Push notification config not found'),
+ { status: 404 }
+ )
+ }
+
+ const result: PushNotificationConfig = {
+ url: config.url,
+ token: config.token || undefined,
+ }
+
+ return NextResponse.json(createResponse(id, result))
+}
+
+/**
+ * Handle tasks/pushNotificationConfig/delete - Delete webhook config for a task
+ */
+async function handlePushNotificationDelete(
+ id: string | number,
+ params: TaskIdParams
+): Promise {
+ if (!params?.id) {
+ return NextResponse.json(
+ createError(id, A2A_ERROR_CODES.INVALID_PARAMS, 'Task ID is required'),
+ { status: 400 }
+ )
+ }
+
+ const [task] = await db.select().from(a2aTask).where(eq(a2aTask.id, params.id)).limit(1)
+
+ if (!task) {
+ return NextResponse.json(createError(id, A2A_ERROR_CODES.TASK_NOT_FOUND, 'Task not found'), {
+ status: 404,
+ })
+ }
+
+ const [config] = await db
+ .select()
+ .from(a2aPushNotificationConfig)
+ .where(eq(a2aPushNotificationConfig.taskId, params.id))
+ .limit(1)
+
+ if (!config) {
+ return NextResponse.json(
+ createError(id, A2A_ERROR_CODES.TASK_NOT_FOUND, 'Push notification config not found'),
+ { status: 404 }
+ )
+ }
+
+ await db.delete(a2aPushNotificationConfig).where(eq(a2aPushNotificationConfig.id, config.id))
+
+ return NextResponse.json(createResponse(id, { success: true }))
+}
diff --git a/apps/sim/app/api/a2a/serve/[agentId]/utils.ts b/apps/sim/app/api/a2a/serve/[agentId]/utils.ts
new file mode 100644
index 0000000000..f157d1efb3
--- /dev/null
+++ b/apps/sim/app/api/a2a/serve/[agentId]/utils.ts
@@ -0,0 +1,176 @@
+import type { Artifact, Message, PushNotificationConfig, Task, TaskState } from '@a2a-js/sdk'
+import { v4 as uuidv4 } from 'uuid'
+import { generateInternalToken } from '@/lib/auth/internal'
+import { getBaseUrl } from '@/lib/core/utils/urls'
+
+/** A2A v0.3 JSON-RPC method names */
+export const A2A_METHODS = {
+ MESSAGE_SEND: 'message/send',
+ MESSAGE_STREAM: 'message/stream',
+ TASKS_GET: 'tasks/get',
+ TASKS_CANCEL: 'tasks/cancel',
+ TASKS_RESUBSCRIBE: 'tasks/resubscribe',
+ PUSH_NOTIFICATION_SET: 'tasks/pushNotificationConfig/set',
+ PUSH_NOTIFICATION_GET: 'tasks/pushNotificationConfig/get',
+ PUSH_NOTIFICATION_DELETE: 'tasks/pushNotificationConfig/delete',
+} as const
+
+/** A2A v0.3 error codes */
+export const A2A_ERROR_CODES = {
+ PARSE_ERROR: -32700,
+ INVALID_REQUEST: -32600,
+ METHOD_NOT_FOUND: -32601,
+ INVALID_PARAMS: -32602,
+ INTERNAL_ERROR: -32603,
+ TASK_NOT_FOUND: -32001,
+ TASK_ALREADY_COMPLETE: -32002,
+ AGENT_UNAVAILABLE: -32003,
+ AUTHENTICATION_REQUIRED: -32004,
+} as const
+
+export interface JSONRPCRequest {
+ jsonrpc: '2.0'
+ id: string | number
+ method: string
+ params?: unknown
+}
+
+export interface JSONRPCResponse {
+ jsonrpc: '2.0'
+ id: string | number | null
+ result?: unknown
+ error?: {
+ code: number
+ message: string
+ data?: unknown
+ }
+}
+
+export interface MessageSendParams {
+ message: Message
+ configuration?: {
+ acceptedOutputModes?: string[]
+ historyLength?: number
+ pushNotificationConfig?: PushNotificationConfig
+ }
+}
+
+export interface TaskIdParams {
+ id: string
+ historyLength?: number
+}
+
+export interface PushNotificationSetParams {
+ id: string
+ pushNotificationConfig: PushNotificationConfig
+}
+
+export function createResponse(id: string | number | null, result: unknown): JSONRPCResponse {
+ return { jsonrpc: '2.0', id, result }
+}
+
+export function createError(
+ id: string | number | null,
+ code: number,
+ message: string,
+ data?: unknown
+): JSONRPCResponse {
+ return { jsonrpc: '2.0', id, error: { code, message, data } }
+}
+
+export function isJSONRPCRequest(obj: unknown): obj is JSONRPCRequest {
+ if (!obj || typeof obj !== 'object') return false
+ const r = obj as Record
+ return r.jsonrpc === '2.0' && typeof r.method === 'string' && r.id !== undefined
+}
+
+export function generateTaskId(): string {
+ return uuidv4()
+}
+
+export function createTaskStatus(state: TaskState): { state: TaskState; timestamp: string } {
+ return { state, timestamp: new Date().toISOString() }
+}
+
+export function formatTaskResponse(task: Task, historyLength?: number): Task {
+ if (historyLength !== undefined && task.history) {
+ return {
+ ...task,
+ history: task.history.slice(-historyLength),
+ }
+ }
+ return task
+}
+
+export interface ExecuteRequestConfig {
+ workflowId: string
+ apiKey?: string | null
+ stream?: boolean
+}
+
+export interface ExecuteRequestResult {
+ url: string
+ headers: Record
+ useInternalAuth: boolean
+}
+
+export async function buildExecuteRequest(
+ config: ExecuteRequestConfig
+): Promise {
+ const url = `${getBaseUrl()}/api/workflows/${config.workflowId}/execute`
+ const headers: Record = { 'Content-Type': 'application/json' }
+ let useInternalAuth = false
+
+ if (config.apiKey) {
+ headers['X-API-Key'] = config.apiKey
+ } else {
+ const internalToken = await generateInternalToken()
+ headers.Authorization = `Bearer ${internalToken}`
+ useInternalAuth = true
+ }
+
+ if (config.stream) {
+ headers['X-Stream-Response'] = 'true'
+ }
+
+ return { url, headers, useInternalAuth }
+}
+
+export function extractAgentContent(executeResult: {
+ output?: { content?: string; [key: string]: unknown }
+ error?: string
+}): string {
+ // Prefer explicit content field
+ if (executeResult.output?.content) {
+ return executeResult.output.content
+ }
+
+ // If output is an object with meaningful data, stringify it
+ if (typeof executeResult.output === 'object' && executeResult.output !== null) {
+ const keys = Object.keys(executeResult.output)
+ // Skip empty objects or objects with only undefined values
+ if (keys.length > 0 && keys.some((k) => executeResult.output![k] !== undefined)) {
+ return JSON.stringify(executeResult.output)
+ }
+ }
+
+ // Fallback to error message or default
+ return executeResult.error || 'Task completed'
+}
+
+export function buildTaskResponse(params: {
+ taskId: string
+ contextId: string
+ state: TaskState
+ history: Message[]
+ artifacts?: Artifact[]
+}): Task {
+ return {
+ kind: 'task',
+ id: params.taskId,
+ contextId: params.contextId,
+ status: createTaskStatus(params.state),
+ history: params.history,
+ artifacts: params.artifacts || [],
+ }
+}
diff --git a/apps/sim/app/api/memory/[id]/route.ts b/apps/sim/app/api/memory/[id]/route.ts
index 617979ef16..2f5b5ae1cc 100644
--- a/apps/sim/app/api/memory/[id]/route.ts
+++ b/apps/sim/app/api/memory/[id]/route.ts
@@ -1,11 +1,12 @@
import { db } from '@sim/db'
-import { memory, permissions, workspace } from '@sim/db/schema'
+import { memory } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { and, eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
+import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils'
const logger = createLogger('MemoryByIdAPI')
@@ -29,46 +30,6 @@ const memoryPutBodySchema = z.object({
workspaceId: z.string().uuid('Invalid workspace ID format'),
})
-async function checkWorkspaceAccess(
- workspaceId: string,
- userId: string
-): Promise<{ hasAccess: boolean; canWrite: boolean }> {
- const [workspaceRow] = await db
- .select({ ownerId: workspace.ownerId })
- .from(workspace)
- .where(eq(workspace.id, workspaceId))
- .limit(1)
-
- if (!workspaceRow) {
- return { hasAccess: false, canWrite: false }
- }
-
- if (workspaceRow.ownerId === userId) {
- return { hasAccess: true, canWrite: true }
- }
-
- const [permissionRow] = await db
- .select({ permissionType: permissions.permissionType })
- .from(permissions)
- .where(
- and(
- eq(permissions.userId, userId),
- eq(permissions.entityType, 'workspace'),
- eq(permissions.entityId, workspaceId)
- )
- )
- .limit(1)
-
- if (!permissionRow) {
- return { hasAccess: false, canWrite: false }
- }
-
- return {
- hasAccess: true,
- canWrite: permissionRow.permissionType === 'write' || permissionRow.permissionType === 'admin',
- }
-}
-
async function validateMemoryAccess(
request: NextRequest,
workspaceId: string,
@@ -86,8 +47,8 @@ async function validateMemoryAccess(
}
}
- const { hasAccess, canWrite } = await checkWorkspaceAccess(workspaceId, authResult.userId)
- if (!hasAccess) {
+ const access = await checkWorkspaceAccess(workspaceId, authResult.userId)
+ if (!access.exists || !access.hasAccess) {
return {
error: NextResponse.json(
{ success: false, error: { message: 'Workspace not found' } },
@@ -96,7 +57,7 @@ async function validateMemoryAccess(
}
}
- if (action === 'write' && !canWrite) {
+ if (action === 'write' && !access.canWrite) {
return {
error: NextResponse.json(
{ success: false, error: { message: 'Write access denied' } },
diff --git a/apps/sim/app/api/memory/route.ts b/apps/sim/app/api/memory/route.ts
index fe159b9664..072756c7a6 100644
--- a/apps/sim/app/api/memory/route.ts
+++ b/apps/sim/app/api/memory/route.ts
@@ -1,56 +1,17 @@
import { db } from '@sim/db'
-import { memory, permissions, workspace } from '@sim/db/schema'
+import { memory } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { and, eq, isNull, like } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
+import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils'
const logger = createLogger('MemoryAPI')
export const dynamic = 'force-dynamic'
export const runtime = 'nodejs'
-async function checkWorkspaceAccess(
- workspaceId: string,
- userId: string
-): Promise<{ hasAccess: boolean; canWrite: boolean }> {
- const [workspaceRow] = await db
- .select({ ownerId: workspace.ownerId })
- .from(workspace)
- .where(eq(workspace.id, workspaceId))
- .limit(1)
-
- if (!workspaceRow) {
- return { hasAccess: false, canWrite: false }
- }
-
- if (workspaceRow.ownerId === userId) {
- return { hasAccess: true, canWrite: true }
- }
-
- const [permissionRow] = await db
- .select({ permissionType: permissions.permissionType })
- .from(permissions)
- .where(
- and(
- eq(permissions.userId, userId),
- eq(permissions.entityType, 'workspace'),
- eq(permissions.entityId, workspaceId)
- )
- )
- .limit(1)
-
- if (!permissionRow) {
- return { hasAccess: false, canWrite: false }
- }
-
- return {
- hasAccess: true,
- canWrite: permissionRow.permissionType === 'write' || permissionRow.permissionType === 'admin',
- }
-}
-
export async function GET(request: NextRequest) {
const requestId = generateRequestId()
@@ -76,8 +37,14 @@ export async function GET(request: NextRequest) {
)
}
- const { hasAccess } = await checkWorkspaceAccess(workspaceId, authResult.userId)
- if (!hasAccess) {
+ const access = await checkWorkspaceAccess(workspaceId, authResult.userId)
+ if (!access.exists) {
+ return NextResponse.json(
+ { success: false, error: { message: 'Workspace not found' } },
+ { status: 404 }
+ )
+ }
+ if (!access.hasAccess) {
return NextResponse.json(
{ success: false, error: { message: 'Access denied to this workspace' } },
{ status: 403 }
@@ -155,15 +122,21 @@ export async function POST(request: NextRequest) {
)
}
- const { hasAccess, canWrite } = await checkWorkspaceAccess(workspaceId, authResult.userId)
- if (!hasAccess) {
+ const access = await checkWorkspaceAccess(workspaceId, authResult.userId)
+ if (!access.exists) {
return NextResponse.json(
{ success: false, error: { message: 'Workspace not found' } },
{ status: 404 }
)
}
+ if (!access.hasAccess) {
+ return NextResponse.json(
+ { success: false, error: { message: 'Access denied to this workspace' } },
+ { status: 403 }
+ )
+ }
- if (!canWrite) {
+ if (!access.canWrite) {
return NextResponse.json(
{ success: false, error: { message: 'Write access denied to this workspace' } },
{ status: 403 }
@@ -282,15 +255,21 @@ export async function DELETE(request: NextRequest) {
)
}
- const { hasAccess, canWrite } = await checkWorkspaceAccess(workspaceId, authResult.userId)
- if (!hasAccess) {
+ const access = await checkWorkspaceAccess(workspaceId, authResult.userId)
+ if (!access.exists) {
return NextResponse.json(
{ success: false, error: { message: 'Workspace not found' } },
{ status: 404 }
)
}
+ if (!access.hasAccess) {
+ return NextResponse.json(
+ { success: false, error: { message: 'Access denied to this workspace' } },
+ { status: 403 }
+ )
+ }
- if (!canWrite) {
+ if (!access.canWrite) {
return NextResponse.json(
{ success: false, error: { message: 'Write access denied to this workspace' } },
{ status: 403 }
diff --git a/apps/sim/app/api/tools/a2a/cancel-task/route.ts b/apps/sim/app/api/tools/a2a/cancel-task/route.ts
new file mode 100644
index 0000000000..9298273cee
--- /dev/null
+++ b/apps/sim/app/api/tools/a2a/cancel-task/route.ts
@@ -0,0 +1,84 @@
+import type { Task } from '@a2a-js/sdk'
+import { createLogger } from '@sim/logger'
+import { type NextRequest, NextResponse } from 'next/server'
+import { z } from 'zod'
+import { createA2AClient } from '@/lib/a2a/utils'
+import { checkHybridAuth } from '@/lib/auth/hybrid'
+import { generateRequestId } from '@/lib/core/utils/request'
+
+const logger = createLogger('A2ACancelTaskAPI')
+
+export const dynamic = 'force-dynamic'
+
+const A2ACancelTaskSchema = z.object({
+ agentUrl: z.string().min(1, 'Agent URL is required'),
+ taskId: z.string().min(1, 'Task ID is required'),
+ apiKey: z.string().optional(),
+})
+
+export async function POST(request: NextRequest) {
+ const requestId = generateRequestId()
+
+ try {
+ const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
+
+ if (!authResult.success) {
+ logger.warn(`[${requestId}] Unauthorized A2A cancel task attempt`)
+ return NextResponse.json(
+ {
+ success: false,
+ error: authResult.error || 'Authentication required',
+ },
+ { status: 401 }
+ )
+ }
+
+ const body = await request.json()
+ const validatedData = A2ACancelTaskSchema.parse(body)
+
+ logger.info(`[${requestId}] Canceling A2A task`, {
+ agentUrl: validatedData.agentUrl,
+ taskId: validatedData.taskId,
+ })
+
+ const client = await createA2AClient(validatedData.agentUrl, validatedData.apiKey)
+
+ const task = (await client.cancelTask({ id: validatedData.taskId })) as Task
+
+ logger.info(`[${requestId}] Successfully canceled A2A task`, {
+ taskId: validatedData.taskId,
+ state: task.status.state,
+ })
+
+ return NextResponse.json({
+ success: true,
+ output: {
+ cancelled: true,
+ state: task.status.state,
+ },
+ })
+ } catch (error) {
+ if (error instanceof z.ZodError) {
+ logger.warn(`[${requestId}] Invalid A2A cancel task request`, {
+ errors: error.errors,
+ })
+ return NextResponse.json(
+ {
+ success: false,
+ error: 'Invalid request data',
+ details: error.errors,
+ },
+ { status: 400 }
+ )
+ }
+
+ logger.error(`[${requestId}] Error canceling A2A task:`, error)
+ return NextResponse.json(
+ {
+ success: false,
+ error: error instanceof Error ? error.message : 'Failed to cancel task',
+ },
+ { status: 500 }
+ )
+ }
+}
diff --git a/apps/sim/app/api/tools/a2a/delete-push-notification/route.ts b/apps/sim/app/api/tools/a2a/delete-push-notification/route.ts
new file mode 100644
index 0000000000..f222ef8830
--- /dev/null
+++ b/apps/sim/app/api/tools/a2a/delete-push-notification/route.ts
@@ -0,0 +1,94 @@
+import { createLogger } from '@sim/logger'
+import { type NextRequest, NextResponse } from 'next/server'
+import { z } from 'zod'
+import { createA2AClient } from '@/lib/a2a/utils'
+import { checkHybridAuth } from '@/lib/auth/hybrid'
+import { generateRequestId } from '@/lib/core/utils/request'
+
+export const dynamic = 'force-dynamic'
+
+const logger = createLogger('A2ADeletePushNotificationAPI')
+
+const A2ADeletePushNotificationSchema = z.object({
+ agentUrl: z.string().min(1, 'Agent URL is required'),
+ taskId: z.string().min(1, 'Task ID is required'),
+ pushNotificationConfigId: z.string().optional(),
+ apiKey: z.string().optional(),
+})
+
+export async function POST(request: NextRequest) {
+ const requestId = generateRequestId()
+
+ try {
+ const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
+
+ if (!authResult.success) {
+ logger.warn(
+ `[${requestId}] Unauthorized A2A delete push notification attempt: ${authResult.error}`
+ )
+ return NextResponse.json(
+ {
+ success: false,
+ error: authResult.error || 'Authentication required',
+ },
+ { status: 401 }
+ )
+ }
+
+ logger.info(
+ `[${requestId}] Authenticated A2A delete push notification request via ${authResult.authType}`,
+ {
+ userId: authResult.userId,
+ }
+ )
+
+ const body = await request.json()
+ const validatedData = A2ADeletePushNotificationSchema.parse(body)
+
+ logger.info(`[${requestId}] Deleting A2A push notification config`, {
+ agentUrl: validatedData.agentUrl,
+ taskId: validatedData.taskId,
+ pushNotificationConfigId: validatedData.pushNotificationConfigId,
+ })
+
+ const client = await createA2AClient(validatedData.agentUrl, validatedData.apiKey)
+
+ await client.deleteTaskPushNotificationConfig({
+ id: validatedData.taskId,
+ pushNotificationConfigId: validatedData.pushNotificationConfigId || validatedData.taskId,
+ })
+
+ logger.info(`[${requestId}] Push notification config deleted successfully`, {
+ taskId: validatedData.taskId,
+ })
+
+ return NextResponse.json({
+ success: true,
+ output: {
+ success: true,
+ },
+ })
+ } catch (error) {
+ if (error instanceof z.ZodError) {
+ logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors })
+ return NextResponse.json(
+ {
+ success: false,
+ error: 'Invalid request data',
+ details: error.errors,
+ },
+ { status: 400 }
+ )
+ }
+
+ logger.error(`[${requestId}] Error deleting A2A push notification:`, error)
+
+ return NextResponse.json(
+ {
+ success: false,
+ error: error instanceof Error ? error.message : 'Failed to delete push notification',
+ },
+ { status: 500 }
+ )
+ }
+}
diff --git a/apps/sim/app/api/tools/a2a/get-agent-card/route.ts b/apps/sim/app/api/tools/a2a/get-agent-card/route.ts
new file mode 100644
index 0000000000..c26ed764b6
--- /dev/null
+++ b/apps/sim/app/api/tools/a2a/get-agent-card/route.ts
@@ -0,0 +1,92 @@
+import { createLogger } from '@sim/logger'
+import { type NextRequest, NextResponse } from 'next/server'
+import { z } from 'zod'
+import { createA2AClient } from '@/lib/a2a/utils'
+import { checkHybridAuth } from '@/lib/auth/hybrid'
+import { generateRequestId } from '@/lib/core/utils/request'
+
+export const dynamic = 'force-dynamic'
+
+const logger = createLogger('A2AGetAgentCardAPI')
+
+const A2AGetAgentCardSchema = z.object({
+ agentUrl: z.string().min(1, 'Agent URL is required'),
+ apiKey: z.string().optional(),
+})
+
+export async function POST(request: NextRequest) {
+ const requestId = generateRequestId()
+
+ try {
+ const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
+
+ if (!authResult.success) {
+ logger.warn(`[${requestId}] Unauthorized A2A get agent card attempt: ${authResult.error}`)
+ return NextResponse.json(
+ {
+ success: false,
+ error: authResult.error || 'Authentication required',
+ },
+ { status: 401 }
+ )
+ }
+
+ logger.info(
+ `[${requestId}] Authenticated A2A get agent card request via ${authResult.authType}`,
+ {
+ userId: authResult.userId,
+ }
+ )
+
+ const body = await request.json()
+ const validatedData = A2AGetAgentCardSchema.parse(body)
+
+ logger.info(`[${requestId}] Fetching Agent Card`, {
+ agentUrl: validatedData.agentUrl,
+ })
+
+ const client = await createA2AClient(validatedData.agentUrl, validatedData.apiKey)
+
+ const agentCard = await client.getAgentCard()
+
+ logger.info(`[${requestId}] Agent Card fetched successfully`, {
+ agentName: agentCard.name,
+ })
+
+ return NextResponse.json({
+ success: true,
+ output: {
+ name: agentCard.name,
+ description: agentCard.description,
+ url: agentCard.url,
+ version: agentCard.protocolVersion,
+ capabilities: agentCard.capabilities,
+ skills: agentCard.skills,
+ defaultInputModes: agentCard.defaultInputModes,
+ defaultOutputModes: agentCard.defaultOutputModes,
+ },
+ })
+ } catch (error) {
+ if (error instanceof z.ZodError) {
+ logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors })
+ return NextResponse.json(
+ {
+ success: false,
+ error: 'Invalid request data',
+ details: error.errors,
+ },
+ { status: 400 }
+ )
+ }
+
+ logger.error(`[${requestId}] Error fetching Agent Card:`, error)
+
+ return NextResponse.json(
+ {
+ success: false,
+ error: error instanceof Error ? error.message : 'Failed to fetch Agent Card',
+ },
+ { status: 500 }
+ )
+ }
+}
diff --git a/apps/sim/app/api/tools/a2a/get-push-notification/route.ts b/apps/sim/app/api/tools/a2a/get-push-notification/route.ts
new file mode 100644
index 0000000000..5feedf4de1
--- /dev/null
+++ b/apps/sim/app/api/tools/a2a/get-push-notification/route.ts
@@ -0,0 +1,115 @@
+import { createLogger } from '@sim/logger'
+import { type NextRequest, NextResponse } from 'next/server'
+import { z } from 'zod'
+import { createA2AClient } from '@/lib/a2a/utils'
+import { checkHybridAuth } from '@/lib/auth/hybrid'
+import { generateRequestId } from '@/lib/core/utils/request'
+
+export const dynamic = 'force-dynamic'
+
+const logger = createLogger('A2AGetPushNotificationAPI')
+
+const A2AGetPushNotificationSchema = z.object({
+ agentUrl: z.string().min(1, 'Agent URL is required'),
+ taskId: z.string().min(1, 'Task ID is required'),
+ apiKey: z.string().optional(),
+})
+
+export async function POST(request: NextRequest) {
+ const requestId = generateRequestId()
+
+ try {
+ const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
+
+ if (!authResult.success) {
+ logger.warn(
+ `[${requestId}] Unauthorized A2A get push notification attempt: ${authResult.error}`
+ )
+ return NextResponse.json(
+ {
+ success: false,
+ error: authResult.error || 'Authentication required',
+ },
+ { status: 401 }
+ )
+ }
+
+ logger.info(
+ `[${requestId}] Authenticated A2A get push notification request via ${authResult.authType}`,
+ {
+ userId: authResult.userId,
+ }
+ )
+
+ const body = await request.json()
+ const validatedData = A2AGetPushNotificationSchema.parse(body)
+
+ logger.info(`[${requestId}] Getting push notification config`, {
+ agentUrl: validatedData.agentUrl,
+ taskId: validatedData.taskId,
+ })
+
+ const client = await createA2AClient(validatedData.agentUrl, validatedData.apiKey)
+
+ const result = await client.getTaskPushNotificationConfig({
+ id: validatedData.taskId,
+ })
+
+ if (!result || !result.pushNotificationConfig) {
+ logger.info(`[${requestId}] No push notification config found for task`, {
+ taskId: validatedData.taskId,
+ })
+ return NextResponse.json({
+ success: true,
+ output: {
+ exists: false,
+ },
+ })
+ }
+
+ logger.info(`[${requestId}] Push notification config retrieved successfully`, {
+ taskId: validatedData.taskId,
+ })
+
+ return NextResponse.json({
+ success: true,
+ output: {
+ url: result.pushNotificationConfig.url,
+ token: result.pushNotificationConfig.token,
+ exists: true,
+ },
+ })
+ } catch (error) {
+ if (error instanceof z.ZodError) {
+ logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors })
+ return NextResponse.json(
+ {
+ success: false,
+ error: 'Invalid request data',
+ details: error.errors,
+ },
+ { status: 400 }
+ )
+ }
+
+ if (error instanceof Error && error.message.includes('not found')) {
+ logger.info(`[${requestId}] Task not found, returning exists: false`)
+ return NextResponse.json({
+ success: true,
+ output: {
+ exists: false,
+ },
+ })
+ }
+
+ logger.error(`[${requestId}] Error getting A2A push notification:`, error)
+
+ return NextResponse.json(
+ {
+ success: false,
+ error: error instanceof Error ? error.message : 'Failed to get push notification',
+ },
+ { status: 500 }
+ )
+ }
+}
diff --git a/apps/sim/app/api/tools/a2a/get-task/route.ts b/apps/sim/app/api/tools/a2a/get-task/route.ts
new file mode 100644
index 0000000000..35aa5e278d
--- /dev/null
+++ b/apps/sim/app/api/tools/a2a/get-task/route.ts
@@ -0,0 +1,95 @@
+import type { Task } from '@a2a-js/sdk'
+import { createLogger } from '@sim/logger'
+import { type NextRequest, NextResponse } from 'next/server'
+import { z } from 'zod'
+import { createA2AClient } from '@/lib/a2a/utils'
+import { checkHybridAuth } from '@/lib/auth/hybrid'
+import { generateRequestId } from '@/lib/core/utils/request'
+
+export const dynamic = 'force-dynamic'
+
+const logger = createLogger('A2AGetTaskAPI')
+
+const A2AGetTaskSchema = z.object({
+ agentUrl: z.string().min(1, 'Agent URL is required'),
+ taskId: z.string().min(1, 'Task ID is required'),
+ apiKey: z.string().optional(),
+ historyLength: z.number().optional(),
+})
+
+export async function POST(request: NextRequest) {
+ const requestId = generateRequestId()
+
+ try {
+ const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
+
+ if (!authResult.success) {
+ logger.warn(`[${requestId}] Unauthorized A2A get task attempt: ${authResult.error}`)
+ return NextResponse.json(
+ {
+ success: false,
+ error: authResult.error || 'Authentication required',
+ },
+ { status: 401 }
+ )
+ }
+
+ logger.info(`[${requestId}] Authenticated A2A get task request via ${authResult.authType}`, {
+ userId: authResult.userId,
+ })
+
+ const body = await request.json()
+ const validatedData = A2AGetTaskSchema.parse(body)
+
+ logger.info(`[${requestId}] Getting A2A task`, {
+ agentUrl: validatedData.agentUrl,
+ taskId: validatedData.taskId,
+ historyLength: validatedData.historyLength,
+ })
+
+ const client = await createA2AClient(validatedData.agentUrl, validatedData.apiKey)
+
+ const task = (await client.getTask({
+ id: validatedData.taskId,
+ historyLength: validatedData.historyLength,
+ })) as Task
+
+ logger.info(`[${requestId}] Successfully retrieved A2A task`, {
+ taskId: task.id,
+ state: task.status.state,
+ })
+
+ return NextResponse.json({
+ success: true,
+ output: {
+ taskId: task.id,
+ contextId: task.contextId,
+ state: task.status.state,
+ artifacts: task.artifacts,
+ history: task.history,
+ },
+ })
+ } catch (error) {
+ if (error instanceof z.ZodError) {
+ logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors })
+ return NextResponse.json(
+ {
+ success: false,
+ error: 'Invalid request data',
+ details: error.errors,
+ },
+ { status: 400 }
+ )
+ }
+
+ logger.error(`[${requestId}] Error getting A2A task:`, error)
+
+ return NextResponse.json(
+ {
+ success: false,
+ error: error instanceof Error ? error.message : 'Failed to get task',
+ },
+ { status: 500 }
+ )
+ }
+}
diff --git a/apps/sim/app/api/tools/a2a/resubscribe/route.ts b/apps/sim/app/api/tools/a2a/resubscribe/route.ts
new file mode 100644
index 0000000000..75c0d24aec
--- /dev/null
+++ b/apps/sim/app/api/tools/a2a/resubscribe/route.ts
@@ -0,0 +1,119 @@
+import type {
+ Artifact,
+ Message,
+ Task,
+ TaskArtifactUpdateEvent,
+ TaskState,
+ TaskStatusUpdateEvent,
+} from '@a2a-js/sdk'
+import { createLogger } from '@sim/logger'
+import { type NextRequest, NextResponse } from 'next/server'
+import { z } from 'zod'
+import { createA2AClient, extractTextContent, isTerminalState } from '@/lib/a2a/utils'
+import { checkHybridAuth } from '@/lib/auth/hybrid'
+import { generateRequestId } from '@/lib/core/utils/request'
+
+const logger = createLogger('A2AResubscribeAPI')
+
+export const dynamic = 'force-dynamic'
+
+const A2AResubscribeSchema = z.object({
+ agentUrl: z.string().min(1, 'Agent URL is required'),
+ taskId: z.string().min(1, 'Task ID is required'),
+ apiKey: z.string().optional(),
+})
+
+export async function POST(request: NextRequest) {
+ const requestId = generateRequestId()
+
+ try {
+ const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
+
+ if (!authResult.success) {
+ logger.warn(`[${requestId}] Unauthorized A2A resubscribe attempt`)
+ return NextResponse.json(
+ {
+ success: false,
+ error: authResult.error || 'Authentication required',
+ },
+ { status: 401 }
+ )
+ }
+
+ const body = await request.json()
+ const validatedData = A2AResubscribeSchema.parse(body)
+
+ const client = await createA2AClient(validatedData.agentUrl, validatedData.apiKey)
+
+ const stream = client.resubscribeTask({ id: validatedData.taskId })
+
+ let taskId = validatedData.taskId
+ let contextId: string | undefined
+ let state: TaskState = 'working'
+ let content = ''
+ let artifacts: Artifact[] = []
+ let history: Message[] = []
+
+ for await (const event of stream) {
+ if (event.kind === 'message') {
+ const msg = event as Message
+ content = extractTextContent(msg)
+ taskId = msg.taskId || taskId
+ contextId = msg.contextId || contextId
+ state = 'completed'
+ } else if (event.kind === 'task') {
+ const task = event as Task
+ taskId = task.id
+ contextId = task.contextId
+ state = task.status.state
+ artifacts = task.artifacts || []
+ history = task.history || []
+ const lastAgentMessage = history.filter((m) => m.role === 'agent').pop()
+ if (lastAgentMessage) {
+ content = extractTextContent(lastAgentMessage)
+ }
+ } else if ('status' in event) {
+ const statusEvent = event as TaskStatusUpdateEvent
+ state = statusEvent.status.state
+ } else if ('artifact' in event) {
+ const artifactEvent = event as TaskArtifactUpdateEvent
+ artifacts.push(artifactEvent.artifact)
+ }
+ }
+
+ logger.info(`[${requestId}] Successfully resubscribed to A2A task ${taskId}`)
+
+ return NextResponse.json({
+ success: true,
+ output: {
+ taskId,
+ contextId,
+ state,
+ isRunning: !isTerminalState(state),
+ artifacts,
+ history,
+ },
+ })
+ } catch (error) {
+ if (error instanceof z.ZodError) {
+ logger.warn(`[${requestId}] Invalid A2A resubscribe data`, { errors: error.errors })
+ return NextResponse.json(
+ {
+ success: false,
+ error: 'Invalid request data',
+ details: error.errors,
+ },
+ { status: 400 }
+ )
+ }
+
+ logger.error(`[${requestId}] Error resubscribing to A2A task:`, error)
+ return NextResponse.json(
+ {
+ success: false,
+ error: error instanceof Error ? error.message : 'Failed to resubscribe',
+ },
+ { status: 500 }
+ )
+ }
+}
diff --git a/apps/sim/app/api/tools/a2a/send-message-stream/route.ts b/apps/sim/app/api/tools/a2a/send-message-stream/route.ts
new file mode 100644
index 0000000000..e30689a801
--- /dev/null
+++ b/apps/sim/app/api/tools/a2a/send-message-stream/route.ts
@@ -0,0 +1,150 @@
+import type {
+ Artifact,
+ Message,
+ Task,
+ TaskArtifactUpdateEvent,
+ TaskState,
+ TaskStatusUpdateEvent,
+} from '@a2a-js/sdk'
+import { createLogger } from '@sim/logger'
+import { type NextRequest, NextResponse } from 'next/server'
+import { z } from 'zod'
+import { createA2AClient, extractTextContent, isTerminalState } from '@/lib/a2a/utils'
+import { checkHybridAuth } from '@/lib/auth/hybrid'
+import { generateRequestId } from '@/lib/core/utils/request'
+
+export const dynamic = 'force-dynamic'
+
+const logger = createLogger('A2ASendMessageStreamAPI')
+
+const A2ASendMessageStreamSchema = z.object({
+ agentUrl: z.string().min(1, 'Agent URL is required'),
+ message: z.string().min(1, 'Message is required'),
+ taskId: z.string().optional(),
+ contextId: z.string().optional(),
+ apiKey: z.string().optional(),
+})
+
+export async function POST(request: NextRequest) {
+ const requestId = generateRequestId()
+
+ try {
+ const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
+
+ if (!authResult.success) {
+ logger.warn(
+ `[${requestId}] Unauthorized A2A send message stream attempt: ${authResult.error}`
+ )
+ return NextResponse.json(
+ {
+ success: false,
+ error: authResult.error || 'Authentication required',
+ },
+ { status: 401 }
+ )
+ }
+
+ logger.info(
+ `[${requestId}] Authenticated A2A send message stream request via ${authResult.authType}`,
+ {
+ userId: authResult.userId,
+ }
+ )
+
+ const body = await request.json()
+ const validatedData = A2ASendMessageStreamSchema.parse(body)
+
+ logger.info(`[${requestId}] Sending A2A streaming message`, {
+ agentUrl: validatedData.agentUrl,
+ hasTaskId: !!validatedData.taskId,
+ hasContextId: !!validatedData.contextId,
+ })
+
+ const client = await createA2AClient(validatedData.agentUrl, validatedData.apiKey)
+
+ const message: Message = {
+ kind: 'message',
+ messageId: crypto.randomUUID(),
+ role: 'user',
+ parts: [{ kind: 'text', text: validatedData.message }],
+ ...(validatedData.taskId && { taskId: validatedData.taskId }),
+ ...(validatedData.contextId && { contextId: validatedData.contextId }),
+ }
+
+ const stream = client.sendMessageStream({ message })
+
+ let taskId = ''
+ let contextId: string | undefined
+ let state: TaskState = 'working'
+ let content = ''
+ let artifacts: Artifact[] = []
+ let history: Message[] = []
+
+ for await (const event of stream) {
+ if (event.kind === 'message') {
+ const msg = event as Message
+ content = extractTextContent(msg)
+ taskId = msg.taskId || taskId
+ contextId = msg.contextId || contextId
+ state = 'completed'
+ } else if (event.kind === 'task') {
+ const task = event as Task
+ taskId = task.id
+ contextId = task.contextId
+ state = task.status.state
+ artifacts = task.artifacts || []
+ history = task.history || []
+ const lastAgentMessage = history.filter((m) => m.role === 'agent').pop()
+ if (lastAgentMessage) {
+ content = extractTextContent(lastAgentMessage)
+ }
+ } else if ('status' in event) {
+ const statusEvent = event as TaskStatusUpdateEvent
+ state = statusEvent.status.state
+ } else if ('artifact' in event) {
+ const artifactEvent = event as TaskArtifactUpdateEvent
+ artifacts.push(artifactEvent.artifact)
+ }
+ }
+
+ logger.info(`[${requestId}] A2A streaming message completed`, {
+ taskId,
+ state,
+ artifactCount: artifacts.length,
+ })
+
+ return NextResponse.json({
+ success: isTerminalState(state) && state !== 'failed',
+ output: {
+ content,
+ taskId,
+ contextId,
+ state,
+ artifacts,
+ history,
+ },
+ })
+ } catch (error) {
+ if (error instanceof z.ZodError) {
+ logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors })
+ return NextResponse.json(
+ {
+ success: false,
+ error: 'Invalid request data',
+ details: error.errors,
+ },
+ { status: 400 }
+ )
+ }
+
+ logger.error(`[${requestId}] Error in A2A streaming:`, error)
+
+ return NextResponse.json(
+ {
+ success: false,
+ error: error instanceof Error ? error.message : 'Streaming failed',
+ },
+ { status: 500 }
+ )
+ }
+}
diff --git a/apps/sim/app/api/tools/a2a/send-message/route.ts b/apps/sim/app/api/tools/a2a/send-message/route.ts
new file mode 100644
index 0000000000..4d52fc710c
--- /dev/null
+++ b/apps/sim/app/api/tools/a2a/send-message/route.ts
@@ -0,0 +1,126 @@
+import type { Message, Task } from '@a2a-js/sdk'
+import { createLogger } from '@sim/logger'
+import { type NextRequest, NextResponse } from 'next/server'
+import { z } from 'zod'
+import { createA2AClient, extractTextContent, isTerminalState } from '@/lib/a2a/utils'
+import { checkHybridAuth } from '@/lib/auth/hybrid'
+import { generateRequestId } from '@/lib/core/utils/request'
+
+export const dynamic = 'force-dynamic'
+
+const logger = createLogger('A2ASendMessageAPI')
+
+const A2ASendMessageSchema = z.object({
+ agentUrl: z.string().min(1, 'Agent URL is required'),
+ message: z.string().min(1, 'Message is required'),
+ taskId: z.string().optional(),
+ contextId: z.string().optional(),
+ apiKey: z.string().optional(),
+})
+
+export async function POST(request: NextRequest) {
+ const requestId = generateRequestId()
+
+ try {
+ const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
+
+ if (!authResult.success) {
+ logger.warn(`[${requestId}] Unauthorized A2A send message attempt: ${authResult.error}`)
+ return NextResponse.json(
+ {
+ success: false,
+ error: authResult.error || 'Authentication required',
+ },
+ { status: 401 }
+ )
+ }
+
+ logger.info(
+ `[${requestId}] Authenticated A2A send message request via ${authResult.authType}`,
+ {
+ userId: authResult.userId,
+ }
+ )
+
+ const body = await request.json()
+ const validatedData = A2ASendMessageSchema.parse(body)
+
+ logger.info(`[${requestId}] Sending A2A message`, {
+ agentUrl: validatedData.agentUrl,
+ hasTaskId: !!validatedData.taskId,
+ hasContextId: !!validatedData.contextId,
+ })
+
+ const client = await createA2AClient(validatedData.agentUrl, validatedData.apiKey)
+
+ const message: Message = {
+ kind: 'message',
+ messageId: crypto.randomUUID(),
+ role: 'user',
+ parts: [{ kind: 'text', text: validatedData.message }],
+ ...(validatedData.taskId && { taskId: validatedData.taskId }),
+ ...(validatedData.contextId && { contextId: validatedData.contextId }),
+ }
+
+ const result = await client.sendMessage({ message })
+
+ if (result.kind === 'message') {
+ const responseMessage = result as Message
+
+ logger.info(`[${requestId}] A2A message sent successfully (message response)`)
+
+ return NextResponse.json({
+ success: true,
+ output: {
+ content: extractTextContent(responseMessage),
+ taskId: responseMessage.taskId || '',
+ contextId: responseMessage.contextId,
+ state: 'completed',
+ },
+ })
+ }
+
+ const task = result as Task
+ const lastAgentMessage = task.history?.filter((m) => m.role === 'agent').pop()
+ const content = lastAgentMessage ? extractTextContent(lastAgentMessage) : ''
+
+ logger.info(`[${requestId}] A2A message sent successfully (task response)`, {
+ taskId: task.id,
+ state: task.status.state,
+ })
+
+ return NextResponse.json({
+ success: isTerminalState(task.status.state) && task.status.state !== 'failed',
+ output: {
+ content,
+ taskId: task.id,
+ contextId: task.contextId,
+ state: task.status.state,
+ artifacts: task.artifacts,
+ history: task.history,
+ },
+ })
+ } catch (error) {
+ if (error instanceof z.ZodError) {
+ logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors })
+ return NextResponse.json(
+ {
+ success: false,
+ error: 'Invalid request data',
+ details: error.errors,
+ },
+ { status: 400 }
+ )
+ }
+
+ logger.error(`[${requestId}] Error sending A2A message:`, error)
+
+ return NextResponse.json(
+ {
+ success: false,
+ error: error instanceof Error ? error.message : 'Internal server error',
+ },
+ { status: 500 }
+ )
+ }
+}
diff --git a/apps/sim/app/api/tools/a2a/set-push-notification/route.ts b/apps/sim/app/api/tools/a2a/set-push-notification/route.ts
new file mode 100644
index 0000000000..d407609418
--- /dev/null
+++ b/apps/sim/app/api/tools/a2a/set-push-notification/route.ts
@@ -0,0 +1,93 @@
+import { createLogger } from '@sim/logger'
+import { type NextRequest, NextResponse } from 'next/server'
+import { z } from 'zod'
+import { createA2AClient } from '@/lib/a2a/utils'
+import { checkHybridAuth } from '@/lib/auth/hybrid'
+import { generateRequestId } from '@/lib/core/utils/request'
+
+export const dynamic = 'force-dynamic'
+
+const logger = createLogger('A2ASetPushNotificationAPI')
+
+const A2ASetPushNotificationSchema = z.object({
+ agentUrl: z.string().min(1, 'Agent URL is required'),
+ taskId: z.string().min(1, 'Task ID is required'),
+ webhookUrl: z.string().min(1, 'Webhook URL is required'),
+ token: z.string().optional(),
+ apiKey: z.string().optional(),
+})
+
+export async function POST(request: NextRequest) {
+ const requestId = generateRequestId()
+
+ try {
+ const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
+
+ if (!authResult.success) {
+ logger.warn(`[${requestId}] Unauthorized A2A set push notification attempt`, {
+ error: authResult.error || 'Authentication required',
+ })
+ return NextResponse.json(
+ {
+ success: false,
+ error: authResult.error || 'Authentication required',
+ },
+ { status: 401 }
+ )
+ }
+
+ const body = await request.json()
+ const validatedData = A2ASetPushNotificationSchema.parse(body)
+
+ logger.info(`[${requestId}] A2A set push notification request`, {
+ agentUrl: validatedData.agentUrl,
+ taskId: validatedData.taskId,
+ webhookUrl: validatedData.webhookUrl,
+ })
+
+ const client = await createA2AClient(validatedData.agentUrl, validatedData.apiKey)
+
+ const result = await client.setTaskPushNotificationConfig({
+ taskId: validatedData.taskId,
+ pushNotificationConfig: {
+ url: validatedData.webhookUrl,
+ token: validatedData.token,
+ },
+ })
+
+ logger.info(`[${requestId}] A2A set push notification successful`, {
+ taskId: validatedData.taskId,
+ })
+
+ return NextResponse.json({
+ success: true,
+ output: {
+ url: result.pushNotificationConfig.url,
+ token: result.pushNotificationConfig.token,
+ success: true,
+ },
+ })
+ } catch (error) {
+ if (error instanceof z.ZodError) {
+ logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors })
+ return NextResponse.json(
+ {
+ success: false,
+ error: 'Invalid request data',
+ details: error.errors,
+ },
+ { status: 400 }
+ )
+ }
+
+ logger.error(`[${requestId}] Error setting A2A push notification:`, error)
+
+ return NextResponse.json(
+ {
+ success: false,
+ error: error instanceof Error ? error.message : 'Failed to set push notification',
+ },
+ { status: 500 }
+ )
+ }
+}
diff --git a/apps/sim/app/api/v1/admin/folders/[id]/export/route.ts b/apps/sim/app/api/v1/admin/folders/[id]/export/route.ts
new file mode 100644
index 0000000000..101d96896e
--- /dev/null
+++ b/apps/sim/app/api/v1/admin/folders/[id]/export/route.ts
@@ -0,0 +1,247 @@
+/**
+ * GET /api/v1/admin/folders/[id]/export
+ *
+ * Export a folder and all its contents (workflows + subfolders) as a ZIP file or JSON (raw, unsanitized for admin backup/restore).
+ *
+ * Query Parameters:
+ * - format: 'zip' (default) or 'json'
+ *
+ * Response:
+ * - ZIP file download (Content-Type: application/zip)
+ * - JSON: FolderExportFullPayload
+ */
+
+import { db } from '@sim/db'
+import { workflow, workflowFolder } from '@sim/db/schema'
+import { createLogger } from '@sim/logger'
+import { eq } from 'drizzle-orm'
+import { NextResponse } from 'next/server'
+import { exportFolderToZip, sanitizePathSegment } from '@/lib/workflows/operations/import-export'
+import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils'
+import { withAdminAuthParams } from '@/app/api/v1/admin/middleware'
+import {
+ internalErrorResponse,
+ notFoundResponse,
+ singleResponse,
+} from '@/app/api/v1/admin/responses'
+import {
+ type FolderExportPayload,
+ parseWorkflowVariables,
+ type WorkflowExportState,
+} from '@/app/api/v1/admin/types'
+
+const logger = createLogger('AdminFolderExportAPI')
+
+interface RouteParams {
+ id: string
+}
+
+interface CollectedWorkflow {
+ id: string
+ folderId: string | null
+}
+
+/**
+ * Recursively collects all workflows within a folder and its subfolders.
+ */
+function collectWorkflowsInFolder(
+ folderId: string,
+ allWorkflows: Array<{ id: string; folderId: string | null }>,
+ allFolders: Array<{ id: string; parentId: string | null }>
+): CollectedWorkflow[] {
+ const collected: CollectedWorkflow[] = []
+
+ for (const wf of allWorkflows) {
+ if (wf.folderId === folderId) {
+ collected.push({ id: wf.id, folderId: wf.folderId })
+ }
+ }
+
+ for (const folder of allFolders) {
+ if (folder.parentId === folderId) {
+ const childWorkflows = collectWorkflowsInFolder(folder.id, allWorkflows, allFolders)
+ collected.push(...childWorkflows)
+ }
+ }
+
+ return collected
+}
+
+/**
+ * Collects all subfolders recursively under a root folder.
+ * Returns folders with parentId adjusted so direct children of rootFolderId have parentId: null.
+ */
+function collectSubfolders(
+ rootFolderId: string,
+ allFolders: Array<{ id: string; name: string; parentId: string | null }>
+): FolderExportPayload[] {
+ const subfolders: FolderExportPayload[] = []
+
+ function collect(parentId: string) {
+ for (const folder of allFolders) {
+ if (folder.parentId === parentId) {
+ subfolders.push({
+ id: folder.id,
+ name: folder.name,
+ parentId: folder.parentId === rootFolderId ? null : folder.parentId,
+ })
+ collect(folder.id)
+ }
+ }
+ }
+
+ collect(rootFolderId)
+ return subfolders
+}
+
+export const GET = withAdminAuthParams(async (request, context) => {
+ const { id: folderId } = await context.params
+ const url = new URL(request.url)
+ const format = url.searchParams.get('format') || 'zip'
+
+ try {
+ const [folderData] = await db
+ .select({
+ id: workflowFolder.id,
+ name: workflowFolder.name,
+ workspaceId: workflowFolder.workspaceId,
+ })
+ .from(workflowFolder)
+ .where(eq(workflowFolder.id, folderId))
+ .limit(1)
+
+ if (!folderData) {
+ return notFoundResponse('Folder')
+ }
+
+ const allWorkflows = await db
+ .select({ id: workflow.id, folderId: workflow.folderId })
+ .from(workflow)
+ .where(eq(workflow.workspaceId, folderData.workspaceId))
+
+ const allFolders = await db
+ .select({
+ id: workflowFolder.id,
+ name: workflowFolder.name,
+ parentId: workflowFolder.parentId,
+ })
+ .from(workflowFolder)
+ .where(eq(workflowFolder.workspaceId, folderData.workspaceId))
+
+ const workflowsInFolder = collectWorkflowsInFolder(folderId, allWorkflows, allFolders)
+ const subfolders = collectSubfolders(folderId, allFolders)
+
+ const workflowExports: Array<{
+ workflow: {
+ id: string
+ name: string
+ description: string | null
+ color: string | null
+ folderId: string | null
+ }
+ state: WorkflowExportState
+ }> = []
+
+ for (const collectedWf of workflowsInFolder) {
+ try {
+ const [wfData] = await db
+ .select()
+ .from(workflow)
+ .where(eq(workflow.id, collectedWf.id))
+ .limit(1)
+
+ if (!wfData) {
+ logger.warn(`Skipping workflow ${collectedWf.id} - not found`)
+ continue
+ }
+
+ const normalizedData = await loadWorkflowFromNormalizedTables(collectedWf.id)
+
+ if (!normalizedData) {
+ logger.warn(`Skipping workflow ${collectedWf.id} - no normalized data found`)
+ continue
+ }
+
+ const variables = parseWorkflowVariables(wfData.variables)
+
+ const remappedFolderId = collectedWf.folderId === folderId ? null : collectedWf.folderId
+
+ const state: WorkflowExportState = {
+ blocks: normalizedData.blocks,
+ edges: normalizedData.edges,
+ loops: normalizedData.loops,
+ parallels: normalizedData.parallels,
+ metadata: {
+ name: wfData.name,
+ description: wfData.description ?? undefined,
+ color: wfData.color,
+ exportedAt: new Date().toISOString(),
+ },
+ variables,
+ }
+
+ workflowExports.push({
+ workflow: {
+ id: wfData.id,
+ name: wfData.name,
+ description: wfData.description,
+ color: wfData.color,
+ folderId: remappedFolderId,
+ },
+ state,
+ })
+ } catch (error) {
+ logger.error(`Failed to load workflow ${collectedWf.id}:`, { error })
+ }
+ }
+
+ logger.info(
+ `Admin API: Exporting folder ${folderId} with ${workflowExports.length} workflows and ${subfolders.length} subfolders`
+ )
+
+ if (format === 'json') {
+ const exportPayload = {
+ version: '1.0',
+ exportedAt: new Date().toISOString(),
+ folder: {
+ id: folderData.id,
+ name: folderData.name,
+ },
+ workflows: workflowExports,
+ folders: subfolders,
+ }
+
+ return singleResponse(exportPayload)
+ }
+
+ const zipWorkflows = workflowExports.map((wf) => ({
+ workflow: {
+ id: wf.workflow.id,
+ name: wf.workflow.name,
+ description: wf.workflow.description ?? undefined,
+ color: wf.workflow.color ?? undefined,
+ folderId: wf.workflow.folderId,
+ },
+ state: wf.state,
+ variables: wf.state.variables,
+ }))
+
+ const zipBlob = await exportFolderToZip(folderData.name, zipWorkflows, subfolders)
+ const arrayBuffer = await zipBlob.arrayBuffer()
+
+ const sanitizedName = sanitizePathSegment(folderData.name)
+ const filename = `${sanitizedName}-${new Date().toISOString().split('T')[0]}.zip`
+
+ return new NextResponse(arrayBuffer, {
+ status: 200,
+ headers: {
+ 'Content-Type': 'application/zip',
+ 'Content-Disposition': `attachment; filename="${filename}"`,
+ 'Content-Length': arrayBuffer.byteLength.toString(),
+ },
+ })
+ } catch (error) {
+ logger.error('Admin API: Failed to export folder', { error, folderId })
+ return internalErrorResponse('Failed to export folder')
+ }
+})
diff --git a/apps/sim/app/api/v1/admin/index.ts b/apps/sim/app/api/v1/admin/index.ts
index e76bece6eb..720c897d82 100644
--- a/apps/sim/app/api/v1/admin/index.ts
+++ b/apps/sim/app/api/v1/admin/index.ts
@@ -34,12 +34,16 @@
* GET /api/v1/admin/workflows/:id - Get workflow details
* DELETE /api/v1/admin/workflows/:id - Delete workflow
* GET /api/v1/admin/workflows/:id/export - Export workflow (JSON)
+ * POST /api/v1/admin/workflows/export - Export multiple workflows (ZIP/JSON)
* POST /api/v1/admin/workflows/import - Import single workflow
* POST /api/v1/admin/workflows/:id/deploy - Deploy workflow
* DELETE /api/v1/admin/workflows/:id/deploy - Undeploy workflow
* GET /api/v1/admin/workflows/:id/versions - List deployment versions
* POST /api/v1/admin/workflows/:id/versions/:vid/activate - Activate specific version
*
+ * Folders:
+ * GET /api/v1/admin/folders/:id/export - Export folder with contents (ZIP/JSON)
+ *
* Organizations:
* GET /api/v1/admin/organizations - List all organizations
* POST /api/v1/admin/organizations - Create organization (requires ownerId)
diff --git a/apps/sim/app/api/v1/admin/workflows/[id]/export/route.ts b/apps/sim/app/api/v1/admin/workflows/[id]/export/route.ts
index 3570cc9f31..565467444b 100644
--- a/apps/sim/app/api/v1/admin/workflows/[id]/export/route.ts
+++ b/apps/sim/app/api/v1/admin/workflows/[id]/export/route.ts
@@ -1,7 +1,7 @@
/**
* GET /api/v1/admin/workflows/[id]/export
*
- * Export a single workflow as JSON.
+ * Export a single workflow as JSON (raw, unsanitized for admin backup/restore).
*
* Response: AdminSingleResponse
*/
diff --git a/apps/sim/app/api/v1/admin/workflows/export/route.ts b/apps/sim/app/api/v1/admin/workflows/export/route.ts
new file mode 100644
index 0000000000..d7cc28babd
--- /dev/null
+++ b/apps/sim/app/api/v1/admin/workflows/export/route.ts
@@ -0,0 +1,147 @@
+/**
+ * POST /api/v1/admin/workflows/export
+ *
+ * Export multiple workflows as a ZIP file or JSON array (raw, unsanitized for admin backup/restore).
+ *
+ * Request Body:
+ * - ids: string[] - Array of workflow IDs to export
+ *
+ * Query Parameters:
+ * - format: 'zip' (default) or 'json'
+ *
+ * Response:
+ * - ZIP file download (Content-Type: application/zip) - each workflow as JSON in root
+ * - JSON: AdminListResponse
+ */
+
+import { db } from '@sim/db'
+import { workflow } from '@sim/db/schema'
+import { createLogger } from '@sim/logger'
+import { inArray } from 'drizzle-orm'
+import JSZip from 'jszip'
+import { NextResponse } from 'next/server'
+import { sanitizePathSegment } from '@/lib/workflows/operations/import-export'
+import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils'
+import { withAdminAuth } from '@/app/api/v1/admin/middleware'
+import {
+ badRequestResponse,
+ internalErrorResponse,
+ listResponse,
+} from '@/app/api/v1/admin/responses'
+import {
+ parseWorkflowVariables,
+ type WorkflowExportPayload,
+ type WorkflowExportState,
+} from '@/app/api/v1/admin/types'
+
+const logger = createLogger('AdminWorkflowsExportAPI')
+
+interface ExportRequest {
+ ids: string[]
+}
+
+export const POST = withAdminAuth(async (request) => {
+ const url = new URL(request.url)
+ const format = url.searchParams.get('format') || 'zip'
+
+ let body: ExportRequest
+ try {
+ body = await request.json()
+ } catch {
+ return badRequestResponse('Invalid JSON body')
+ }
+
+ if (!body.ids || !Array.isArray(body.ids) || body.ids.length === 0) {
+ return badRequestResponse('ids must be a non-empty array of workflow IDs')
+ }
+
+ try {
+ const workflows = await db.select().from(workflow).where(inArray(workflow.id, body.ids))
+
+ if (workflows.length === 0) {
+ return badRequestResponse('No workflows found with the provided IDs')
+ }
+
+ const workflowExports: WorkflowExportPayload[] = []
+
+ for (const wf of workflows) {
+ try {
+ const normalizedData = await loadWorkflowFromNormalizedTables(wf.id)
+
+ if (!normalizedData) {
+ logger.warn(`Skipping workflow ${wf.id} - no normalized data found`)
+ continue
+ }
+
+ const variables = parseWorkflowVariables(wf.variables)
+
+ const state: WorkflowExportState = {
+ blocks: normalizedData.blocks,
+ edges: normalizedData.edges,
+ loops: normalizedData.loops,
+ parallels: normalizedData.parallels,
+ metadata: {
+ name: wf.name,
+ description: wf.description ?? undefined,
+ color: wf.color,
+ exportedAt: new Date().toISOString(),
+ },
+ variables,
+ }
+
+ const exportPayload: WorkflowExportPayload = {
+ version: '1.0',
+ exportedAt: new Date().toISOString(),
+ workflow: {
+ id: wf.id,
+ name: wf.name,
+ description: wf.description,
+ color: wf.color,
+ workspaceId: wf.workspaceId,
+ folderId: wf.folderId,
+ },
+ state,
+ }
+
+ workflowExports.push(exportPayload)
+ } catch (error) {
+ logger.error(`Failed to load workflow ${wf.id}:`, { error })
+ }
+ }
+
+ logger.info(`Admin API: Exporting ${workflowExports.length} workflows`)
+
+ if (format === 'json') {
+ return listResponse(workflowExports, {
+ total: workflowExports.length,
+ limit: workflowExports.length,
+ offset: 0,
+ hasMore: false,
+ })
+ }
+
+ const zip = new JSZip()
+
+ for (const exportPayload of workflowExports) {
+ const filename = `${sanitizePathSegment(exportPayload.workflow.name)}.json`
+ zip.file(filename, JSON.stringify(exportPayload, null, 2))
+ }
+
+ const zipBlob = await zip.generateAsync({ type: 'blob' })
+ const arrayBuffer = await zipBlob.arrayBuffer()
+
+ const filename = `workflows-export-${new Date().toISOString().split('T')[0]}.zip`
+
+ return new NextResponse(arrayBuffer, {
+ status: 200,
+ headers: {
+ 'Content-Type': 'application/zip',
+ 'Content-Disposition': `attachment; filename="${filename}"`,
+ 'Content-Length': arrayBuffer.byteLength.toString(),
+ },
+ })
+ } catch (error) {
+ logger.error('Admin API: Failed to export workflows', { error, ids: body.ids })
+ return internalErrorResponse('Failed to export workflows')
+ }
+})
diff --git a/apps/sim/app/api/v1/admin/workspaces/[id]/export/route.ts b/apps/sim/app/api/v1/admin/workspaces/[id]/export/route.ts
index f7e60502ad..6cd9055630 100644
--- a/apps/sim/app/api/v1/admin/workspaces/[id]/export/route.ts
+++ b/apps/sim/app/api/v1/admin/workspaces/[id]/export/route.ts
@@ -1,7 +1,7 @@
/**
* GET /api/v1/admin/workspaces/[id]/export
*
- * Export an entire workspace as a ZIP file or JSON.
+ * Export an entire workspace as a ZIP file or JSON (raw, unsanitized for admin backup/restore).
*
* Query Parameters:
* - format: 'zip' (default) or 'json'
@@ -16,7 +16,7 @@ import { workflow, workflowFolder, workspace } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { eq } from 'drizzle-orm'
import { NextResponse } from 'next/server'
-import { exportWorkspaceToZip } from '@/lib/workflows/operations/import-export'
+import { exportWorkspaceToZip, sanitizePathSegment } from '@/lib/workflows/operations/import-export'
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils'
import { withAdminAuthParams } from '@/app/api/v1/admin/middleware'
import {
@@ -146,7 +146,7 @@ export const GET = withAdminAuthParams(async (request, context) =>
const zipBlob = await exportWorkspaceToZip(workspaceData.name, zipWorkflows, folderExports)
const arrayBuffer = await zipBlob.arrayBuffer()
- const sanitizedName = workspaceData.name.replace(/[^a-z0-9-_]/gi, '-')
+ const sanitizedName = sanitizePathSegment(workspaceData.name)
const filename = `${sanitizedName}-${new Date().toISOString().split('T')[0]}.zip`
return new NextResponse(arrayBuffer, {
diff --git a/apps/sim/app/api/workflows/[id]/execute/route.ts b/apps/sim/app/api/workflows/[id]/execute/route.ts
index ca35437e5a..3a9b04dfba 100644
--- a/apps/sim/app/api/workflows/[id]/execute/route.ts
+++ b/apps/sim/app/api/workflows/[id]/execute/route.ts
@@ -27,7 +27,7 @@ import { ExecutionSnapshot } from '@/executor/execution/snapshot'
import type { ExecutionMetadata, IterationContext } from '@/executor/execution/types'
import type { StreamingExecution } from '@/executor/types'
import { Serializer } from '@/serializer'
-import { CORE_TRIGGER_TYPES } from '@/stores/logs/filters/types'
+import { CORE_TRIGGER_TYPES, type CoreTriggerType } from '@/stores/logs/filters/types'
const logger = createLogger('WorkflowExecuteAPI')
@@ -109,7 +109,7 @@ type AsyncExecutionParams = {
workflowId: string
userId: string
input: any
- triggerType: 'api' | 'webhook' | 'schedule' | 'manual' | 'chat' | 'mcp'
+ triggerType: CoreTriggerType
}
/**
@@ -215,10 +215,10 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
workflowStateOverride,
} = validation.data
- // For API key auth, the entire body is the input (except for our control fields)
+ // For API key and internal JWT auth, the entire body is the input (except for our control fields)
// For session auth, the input is explicitly provided in the input field
const input =
- auth.authType === 'api_key'
+ auth.authType === 'api_key' || auth.authType === 'internal_jwt'
? (() => {
const {
selectedOutputs,
@@ -226,6 +226,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
stream,
useDraftState,
workflowStateOverride,
+ workflowId: _workflowId, // Also exclude workflowId used for internal JWT auth
...rest
} = body
return Object.keys(rest).length > 0 ? rest : validatedInput
@@ -252,17 +253,9 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
})
const executionId = uuidv4()
- type LoggingTriggerType = 'api' | 'webhook' | 'schedule' | 'manual' | 'chat' | 'mcp'
- let loggingTriggerType: LoggingTriggerType = 'manual'
- if (
- triggerType === 'api' ||
- triggerType === 'chat' ||
- triggerType === 'webhook' ||
- triggerType === 'schedule' ||
- triggerType === 'manual' ||
- triggerType === 'mcp'
- ) {
- loggingTriggerType = triggerType as LoggingTriggerType
+ let loggingTriggerType: CoreTriggerType = 'manual'
+ if (CORE_TRIGGER_TYPES.includes(triggerType as CoreTriggerType)) {
+ loggingTriggerType = triggerType as CoreTriggerType
}
const loggingSession = new LoggingSession(
workflowId,
diff --git a/apps/sim/app/api/workflows/route.ts b/apps/sim/app/api/workflows/route.ts
index 7c905ab7e6..81d4c885b9 100644
--- a/apps/sim/app/api/workflows/route.ts
+++ b/apps/sim/app/api/workflows/route.ts
@@ -1,12 +1,12 @@
import { db } from '@sim/db'
-import { workflow, workspace } from '@sim/db/schema'
+import { workflow } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { getSession } from '@/lib/auth'
import { generateRequestId } from '@/lib/core/utils/request'
-import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
+import { getUserEntityPermissions, workspaceExists } from '@/lib/workspaces/permissions/utils'
import { verifyWorkspaceMembership } from '@/app/api/workflows/utils'
const logger = createLogger('WorkflowAPI')
@@ -36,13 +36,9 @@ export async function GET(request: Request) {
const userId = session.user.id
if (workspaceId) {
- const workspaceExists = await db
- .select({ id: workspace.id })
- .from(workspace)
- .where(eq(workspace.id, workspaceId))
- .then((rows) => rows.length > 0)
+ const wsExists = await workspaceExists(workspaceId)
- if (!workspaceExists) {
+ if (!wsExists) {
logger.warn(
`[${requestId}] Attempt to fetch workflows for non-existent workspace: ${workspaceId}`
)
diff --git a/apps/sim/app/api/workspaces/[id]/api-keys/route.ts b/apps/sim/app/api/workspaces/[id]/api-keys/route.ts
index 1232272366..c649972140 100644
--- a/apps/sim/app/api/workspaces/[id]/api-keys/route.ts
+++ b/apps/sim/app/api/workspaces/[id]/api-keys/route.ts
@@ -1,5 +1,5 @@
import { db } from '@sim/db'
-import { apiKey, workspace } from '@sim/db/schema'
+import { apiKey } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { and, eq, inArray } from 'drizzle-orm'
import { nanoid } from 'nanoid'
@@ -9,7 +9,7 @@ import { createApiKey, getApiKeyDisplayFormat } from '@/lib/api-key/auth'
import { getSession } from '@/lib/auth'
import { PlatformEvents } from '@/lib/core/telemetry'
import { generateRequestId } from '@/lib/core/utils/request'
-import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
+import { getUserEntityPermissions, getWorkspaceById } from '@/lib/workspaces/permissions/utils'
const logger = createLogger('WorkspaceApiKeysAPI')
@@ -34,8 +34,8 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
const userId = session.user.id
- const ws = await db.select().from(workspace).where(eq(workspace.id, workspaceId)).limit(1)
- if (!ws.length) {
+ const ws = await getWorkspaceById(workspaceId)
+ if (!ws) {
return NextResponse.json({ error: 'Workspace not found' }, { status: 404 })
}
diff --git a/apps/sim/app/api/workspaces/[id]/byok-keys/route.ts b/apps/sim/app/api/workspaces/[id]/byok-keys/route.ts
index 246cc6b245..3078555350 100644
--- a/apps/sim/app/api/workspaces/[id]/byok-keys/route.ts
+++ b/apps/sim/app/api/workspaces/[id]/byok-keys/route.ts
@@ -1,5 +1,5 @@
import { db } from '@sim/db'
-import { workspace, workspaceBYOKKeys } from '@sim/db/schema'
+import { workspaceBYOKKeys } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { and, eq } from 'drizzle-orm'
import { nanoid } from 'nanoid'
@@ -8,7 +8,7 @@ import { z } from 'zod'
import { getSession } from '@/lib/auth'
import { decryptSecret, encryptSecret } from '@/lib/core/security/encryption'
import { generateRequestId } from '@/lib/core/utils/request'
-import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
+import { getUserEntityPermissions, getWorkspaceById } from '@/lib/workspaces/permissions/utils'
const logger = createLogger('WorkspaceBYOKKeysAPI')
@@ -46,8 +46,8 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
const userId = session.user.id
- const ws = await db.select().from(workspace).where(eq(workspace.id, workspaceId)).limit(1)
- if (!ws.length) {
+ const ws = await getWorkspaceById(workspaceId)
+ if (!ws) {
return NextResponse.json({ error: 'Workspace not found' }, { status: 404 })
}
diff --git a/apps/sim/app/api/workspaces/[id]/environment/route.ts b/apps/sim/app/api/workspaces/[id]/environment/route.ts
index 9c1ee4eb04..f11da0ecc9 100644
--- a/apps/sim/app/api/workspaces/[id]/environment/route.ts
+++ b/apps/sim/app/api/workspaces/[id]/environment/route.ts
@@ -1,5 +1,5 @@
import { db } from '@sim/db'
-import { environment, workspace, workspaceEnvironment } from '@sim/db/schema'
+import { environment, workspaceEnvironment } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
@@ -7,7 +7,7 @@ import { z } from 'zod'
import { getSession } from '@/lib/auth'
import { decryptSecret, encryptSecret } from '@/lib/core/security/encryption'
import { generateRequestId } from '@/lib/core/utils/request'
-import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
+import { getUserEntityPermissions, getWorkspaceById } from '@/lib/workspaces/permissions/utils'
const logger = createLogger('WorkspaceEnvironmentAPI')
@@ -33,8 +33,8 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
const userId = session.user.id
// Validate workspace exists
- const ws = await db.select().from(workspace).where(eq(workspace.id, workspaceId)).limit(1)
- if (!ws.length) {
+ const ws = await getWorkspaceById(workspaceId)
+ if (!ws) {
return NextResponse.json({ error: 'Workspace not found' }, { status: 404 })
}
diff --git a/apps/sim/app/page.tsx b/apps/sim/app/page.tsx
index 88a57bfd65..4bd1e643a4 100644
--- a/apps/sim/app/page.tsx
+++ b/apps/sim/app/page.tsx
@@ -11,9 +11,9 @@ export const metadata: Metadata = {
'Open-source AI agent workflow builder used by 60,000+ developers. Build and deploy agentic workflows with a visual drag-and-drop canvas. Connect 100+ apps and ship SOC2 & HIPAA-ready AI automations from startups to Fortune 500.',
keywords:
'AI agent workflow builder, agentic workflows, open source AI, visual workflow builder, AI automation, LLM workflows, AI agents, workflow automation, no-code AI, SOC2 compliant, HIPAA compliant, enterprise AI',
- authors: [{ name: 'Sim Studio' }],
- creator: 'Sim Studio',
- publisher: 'Sim Studio',
+ authors: [{ name: 'Sim' }],
+ creator: 'Sim',
+ publisher: 'Sim',
formatDetection: {
email: false,
address: false,
diff --git a/apps/sim/app/playground/page.tsx b/apps/sim/app/playground/page.tsx
index a1a6694cfb..4670b805e0 100644
--- a/apps/sim/app/playground/page.tsx
+++ b/apps/sim/app/playground/page.tsx
@@ -364,12 +364,30 @@ export default function PlaygroundPage() {
+
{}} />
+ {}} />
{}} />
+
+
+ true}
+ onRemove={() => {}}
+ placeholder='Add tags'
+ placeholderWithTags='Add another'
+ tagVariant='secondary'
+ triggerKeys={['Enter', ',']}
+ />
+
+
['va
schedule: 'green',
chat: 'purple',
webhook: 'orange',
+ a2a: 'teal',
}
interface StatusBadgeProps {
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat.tsx
index 1eba78595d..feebe135e4 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat.tsx
@@ -888,7 +888,7 @@ export function Chat() {
selectedOutputs={selectedOutputs}
onOutputSelect={handleOutputSelection}
disabled={!activeWorkflowId}
- placeholder='Select outputs'
+ placeholder='Outputs'
align='end'
maxHeight={180}
/>
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/components/output-select/output-select.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/components/output-select/output-select.tsx
index 43fee221a3..cf6973216e 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/components/output-select/output-select.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/components/output-select/output-select.tsx
@@ -1,16 +1,9 @@
'use client'
import type React from 'react'
-import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
-import { Check, RepeatIcon, SplitIcon } from 'lucide-react'
-import {
- Badge,
- Popover,
- PopoverContent,
- PopoverDivider,
- PopoverItem,
- PopoverTrigger,
-} from '@/components/emcn'
+import { useMemo } from 'react'
+import { RepeatIcon, SplitIcon } from 'lucide-react'
+import { Combobox, type ComboboxOptionGroup } from '@/components/emcn'
import {
extractFieldsFromSchema,
parseResponseFormatSafely,
@@ -21,7 +14,7 @@ import { useSubBlockStore } from '@/stores/workflows/subblock/store'
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
/**
- * Renders a tag icon with background color.
+ * Renders a tag icon with background color for block section headers.
*
* @param icon - Either a letter string or a Lucide icon component
* @param color - Background color for the icon container
@@ -62,14 +55,9 @@ interface OutputSelectProps {
placeholder?: string
/** Whether to emit output IDs or labels in onOutputSelect callback */
valueMode?: 'id' | 'label'
- /**
- * When true, renders the underlying popover content inline instead of in a portal.
- * Useful when used inside dialogs or other portalled components that manage scroll locking.
- */
- disablePopoverPortal?: boolean
- /** Alignment of the popover relative to the trigger */
+ /** Alignment of the dropdown relative to the trigger */
align?: 'start' | 'end' | 'center'
- /** Maximum height of the popover content in pixels */
+ /** Maximum height of the dropdown content in pixels */
maxHeight?: number
}
@@ -90,14 +78,9 @@ export function OutputSelect({
disabled = false,
placeholder = 'Select outputs',
valueMode = 'id',
- disablePopoverPortal = false,
align = 'start',
maxHeight = 200,
}: OutputSelectProps) {
- const [open, setOpen] = useState(false)
- const [highlightedIndex, setHighlightedIndex] = useState(-1)
- const triggerRef = useRef(null)
- const popoverRef = useRef(null)
const blocks = useWorkflowStore((state) => state.blocks)
const { isShowingDiff, isDiffReady, hasActiveDiff, baselineWorkflow } = useWorkflowDiffStore()
const subBlockValues = useSubBlockStore((state) =>
@@ -206,21 +189,10 @@ export function OutputSelect({
shouldUseBaseline,
])
- /**
- * Checks if an output is currently selected by comparing both ID and label
- * @param o - The output object to check
- * @returns True if the output is selected, false otherwise
- */
- const isSelectedValue = useCallback(
- (o: { id: string; label: string }) =>
- selectedOutputs.includes(o.id) || selectedOutputs.includes(o.label),
- [selectedOutputs]
- )
-
/**
* Gets display text for selected outputs
*/
- const selectedOutputsDisplayText = useMemo(() => {
+ const selectedDisplayText = useMemo(() => {
if (!selectedOutputs || selectedOutputs.length === 0) {
return placeholder
}
@@ -234,19 +206,27 @@ export function OutputSelect({
}
if (validOutputs.length === 1) {
- const output = workflowOutputs.find(
- (o) => o.id === validOutputs[0] || o.label === validOutputs[0]
- )
- return output?.label || placeholder
+ return '1 output'
}
return `${validOutputs.length} outputs`
}, [selectedOutputs, workflowOutputs, placeholder])
/**
- * Groups outputs by block and sorts by distance from starter block
+ * Gets the background color for a block output based on its type
+ * @param blockType - The type of the block
+ * @returns The hex color code for the block
*/
- const groupedOutputs = useMemo(() => {
+ const getOutputColor = (blockType: string) => {
+ const blockConfig = getBlock(blockType)
+ return blockConfig?.bgColor || '#2F55FF'
+ }
+
+ /**
+ * Groups outputs by block and sorts by distance from starter block.
+ * Returns ComboboxOptionGroup[] for use with Combobox.
+ */
+ const comboboxGroups = useMemo((): ComboboxOptionGroup[] => {
const groups: Record = {}
const blockDistances: Record = {}
const edges = useWorkflowStore.getState().edges
@@ -283,242 +263,75 @@ export function OutputSelect({
groups[output.blockName].push(output)
})
- return Object.entries(groups)
+ const sortedGroups = Object.entries(groups)
.map(([blockName, outputs]) => ({
blockName,
outputs,
distance: blockDistances[outputs[0]?.blockId] || 0,
}))
.sort((a, b) => b.distance - a.distance)
- .reduce(
- (acc, { blockName, outputs }) => {
- acc[blockName] = outputs
- return acc
- },
- {} as Record
- )
- }, [workflowOutputs, blocks])
-
- /**
- * Gets the background color for a block output based on its type
- * @param blockId - The block ID (unused but kept for future extensibility)
- * @param blockType - The type of the block
- * @returns The hex color code for the block
- */
- const getOutputColor = (blockId: string, blockType: string) => {
- const blockConfig = getBlock(blockType)
- return blockConfig?.bgColor || '#2F55FF'
- }
-
- /**
- * Flattened outputs for keyboard navigation
- */
- const flattenedOutputs = useMemo(() => {
- return Object.values(groupedOutputs).flat()
- }, [groupedOutputs])
-
- /**
- * Handles output selection by toggling the selected state
- * @param value - The output label to toggle
- */
- const handleOutputSelection = useCallback(
- (value: string) => {
- const emittedValue =
- valueMode === 'label' ? value : workflowOutputs.find((o) => o.label === value)?.id || value
- const index = selectedOutputs.indexOf(emittedValue)
-
- const newSelectedOutputs =
- index === -1
- ? [...new Set([...selectedOutputs, emittedValue])]
- : selectedOutputs.filter((id) => id !== emittedValue)
-
- onOutputSelect(newSelectedOutputs)
- },
- [valueMode, workflowOutputs, selectedOutputs, onOutputSelect]
- )
-
- /**
- * Handles keyboard navigation within the output list
- * Supports ArrowUp, ArrowDown, Enter, and Escape keys
- */
- useEffect(() => {
- if (!open || flattenedOutputs.length === 0) return
-
- const handleKeyboardEvent = (e: KeyboardEvent) => {
- switch (e.key) {
- case 'ArrowDown':
- e.preventDefault()
- e.stopPropagation()
- setHighlightedIndex((prev) => {
- if (prev === -1 || prev >= flattenedOutputs.length - 1) {
- return 0
- }
- return prev + 1
- })
- break
-
- case 'ArrowUp':
- e.preventDefault()
- e.stopPropagation()
- setHighlightedIndex((prev) => {
- if (prev <= 0) {
- return flattenedOutputs.length - 1
- }
- return prev - 1
- })
- break
-
- case 'Enter':
- e.preventDefault()
- e.stopPropagation()
- setHighlightedIndex((currentIndex) => {
- if (currentIndex >= 0 && currentIndex < flattenedOutputs.length) {
- handleOutputSelection(flattenedOutputs[currentIndex].label)
- }
- return currentIndex
- })
- break
- case 'Escape':
- e.preventDefault()
- e.stopPropagation()
- setOpen(false)
- break
+ return sortedGroups.map(({ blockName, outputs }) => {
+ const firstOutput = outputs[0]
+ const blockConfig = getBlock(firstOutput.blockType)
+ const blockColor = getOutputColor(firstOutput.blockType)
+
+ let blockIcon: string | React.ComponentType<{ className?: string }> = blockName
+ .charAt(0)
+ .toUpperCase()
+
+ if (blockConfig?.icon) {
+ blockIcon = blockConfig.icon
+ } else if (firstOutput.blockType === 'loop') {
+ blockIcon = RepeatIcon
+ } else if (firstOutput.blockType === 'parallel') {
+ blockIcon = SplitIcon
}
- }
-
- window.addEventListener('keydown', handleKeyboardEvent, true)
- return () => window.removeEventListener('keydown', handleKeyboardEvent, true)
- }, [open, flattenedOutputs, handleOutputSelection])
- /**
- * Reset highlighted index when popover opens/closes
- */
- useEffect(() => {
- if (open) {
- const firstSelectedIndex = flattenedOutputs.findIndex((output) => isSelectedValue(output))
- setHighlightedIndex(firstSelectedIndex >= 0 ? firstSelectedIndex : -1)
- } else {
- setHighlightedIndex(-1)
- }
- }, [open, flattenedOutputs, isSelectedValue])
-
- /**
- * Scroll highlighted item into view
- */
- useEffect(() => {
- if (highlightedIndex >= 0 && popoverRef.current) {
- const highlightedElement = popoverRef.current.querySelector(
- `[data-option-index="${highlightedIndex}"]`
- )
- if (highlightedElement) {
- highlightedElement.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
+ return {
+ sectionElement: (
+
+
+ {blockName}
+
+ ),
+ items: outputs.map((output) => ({
+ label: output.path,
+ value: valueMode === 'label' ? output.label : output.id,
+ })),
}
- }
- }, [highlightedIndex])
+ })
+ }, [workflowOutputs, blocks, valueMode])
/**
- * Closes popover when clicking outside
+ * Normalize selected values to match the valueMode
*/
- useEffect(() => {
- if (!open) return
-
- const handleClickOutside = (event: MouseEvent) => {
- const target = event.target as Node
- const insideTrigger = triggerRef.current?.contains(target)
- const insidePopover = popoverRef.current?.contains(target)
-
- if (!insideTrigger && !insidePopover) {
- setOpen(false)
- }
- }
-
- document.addEventListener('mousedown', handleClickOutside)
- return () => document.removeEventListener('mousedown', handleClickOutside)
- }, [open])
+ const normalizedSelectedValues = useMemo(() => {
+ return selectedOutputs
+ .map((val) => {
+ // Find the output that matches either id or label
+ const output = workflowOutputs.find((o) => o.id === val || o.label === val)
+ if (!output) return null
+ // Return in the format matching valueMode
+ return valueMode === 'label' ? output.label : output.id
+ })
+ .filter((v): v is string => v !== null)
+ }, [selectedOutputs, workflowOutputs, valueMode])
return (
-
-
-
- {
- if (disabled || workflowOutputs.length === 0) return
- e.stopPropagation()
- setOpen((prev) => !prev)
- }}
- >
- {selectedOutputsDisplayText}
-
-
-
-
-
- {Object.entries(groupedOutputs).map(([blockName, outputs], groupIndex, groupArray) => {
- const startIndex = flattenedOutputs.findIndex((o) => o.blockName === blockName)
-
- const firstOutput = outputs[0]
- const blockConfig = getBlock(firstOutput.blockType)
- const blockColor = getOutputColor(firstOutput.blockId, firstOutput.blockType)
-
- let blockIcon: string | React.ComponentType<{ className?: string }> = blockName
- .charAt(0)
- .toUpperCase()
-
- if (blockConfig?.icon) {
- blockIcon = blockConfig.icon
- } else if (firstOutput.blockType === 'loop') {
- blockIcon = RepeatIcon
- } else if (firstOutput.blockType === 'parallel') {
- blockIcon = SplitIcon
- }
-
- return (
-
-
-
- {blockName}
-
-
-
- {outputs.map((output, localIndex) => {
- const globalIndex = startIndex + localIndex
- const isHighlighted = globalIndex === highlightedIndex
-
- return (
-
handleOutputSelection(output.label)}
- onMouseEnter={() => setHighlightedIndex(globalIndex)}
- >
- {output.path}
- {isSelectedValue(output) && }
-
- )
- })}
-
- {groupIndex < groupArray.length - 1 &&
}
-
- )
- })}
-
-
-
+
)
}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/a2a/a2a.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/a2a/a2a.tsx
new file mode 100644
index 0000000000..81fd828842
--- /dev/null
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/a2a/a2a.tsx
@@ -0,0 +1,941 @@
+'use client'
+
+import { useCallback, useEffect, useMemo, useState } from 'react'
+import { createLogger } from '@sim/logger'
+import { Check, Clipboard } from 'lucide-react'
+import { useParams } from 'next/navigation'
+import {
+ Badge,
+ Button,
+ ButtonGroup,
+ ButtonGroupItem,
+ Checkbox,
+ Code,
+ Combobox,
+ type ComboboxOption,
+ Input,
+ Label,
+ TagInput,
+ Textarea,
+ Tooltip,
+} from '@/components/emcn'
+import { Skeleton } from '@/components/ui'
+import type { AgentAuthentication, AgentCapabilities } from '@/lib/a2a/types'
+import { getBaseUrl } from '@/lib/core/utils/urls'
+import { normalizeInputFormatValue } from '@/lib/workflows/input-format-utils'
+import { StartBlockPath, TriggerUtils } from '@/lib/workflows/triggers/triggers'
+import {
+ useA2AAgentByWorkflow,
+ useCreateA2AAgent,
+ useDeleteA2AAgent,
+ usePublishA2AAgent,
+ useUpdateA2AAgent,
+} from '@/hooks/queries/a2a/agents'
+import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
+import { useSubBlockStore } from '@/stores/workflows/subblock/store'
+import { useWorkflowStore } from '@/stores/workflows/workflow/store'
+
+const logger = createLogger('A2ADeploy')
+
+interface InputFormatField {
+ id?: string
+ name?: string
+ type?: string
+ value?: unknown
+ collapsed?: boolean
+}
+
+/**
+ * Check if a description is a default/placeholder value that should be filtered out
+ */
+function isDefaultDescription(desc: string | null | undefined, workflowName: string): boolean {
+ if (!desc) return true
+ const normalized = desc.toLowerCase().trim()
+ return (
+ normalized === '' || normalized === 'new workflow' || normalized === workflowName.toLowerCase()
+ )
+}
+
+type CodeLanguage = 'curl' | 'python' | 'javascript' | 'typescript'
+
+const LANGUAGE_LABELS: Record = {
+ curl: 'cURL',
+ python: 'Python',
+ javascript: 'JavaScript',
+ typescript: 'TypeScript',
+}
+
+const LANGUAGE_SYNTAX: Record = {
+ curl: 'javascript',
+ python: 'python',
+ javascript: 'javascript',
+ typescript: 'javascript',
+}
+
+interface A2aDeployProps {
+ workflowId: string
+ workflowName: string
+ workflowDescription?: string | null
+ isDeployed: boolean
+ workflowNeedsRedeployment?: boolean
+ onSubmittingChange?: (submitting: boolean) => void
+ onCanSaveChange?: (canSave: boolean) => void
+ onAgentExistsChange?: (exists: boolean) => void
+ onPublishedChange?: (published: boolean) => void
+ onNeedsRepublishChange?: (needsRepublish: boolean) => void
+ onDeployWorkflow?: () => Promise
+}
+
+type AuthScheme = 'none' | 'apiKey'
+
+export function A2aDeploy({
+ workflowId,
+ workflowName,
+ workflowDescription,
+ isDeployed,
+ workflowNeedsRedeployment,
+ onSubmittingChange,
+ onCanSaveChange,
+ onAgentExistsChange,
+ onPublishedChange,
+ onNeedsRepublishChange,
+ onDeployWorkflow,
+}: A2aDeployProps) {
+ const params = useParams()
+ const workspaceId = params.workspaceId as string
+
+ const { data: existingAgent, isLoading } = useA2AAgentByWorkflow(workspaceId, workflowId)
+
+ const createAgent = useCreateA2AAgent()
+ const updateAgent = useUpdateA2AAgent()
+ const deleteAgent = useDeleteA2AAgent()
+ const publishAgent = usePublishA2AAgent()
+
+ const blocks = useWorkflowStore((state) => state.blocks)
+ const { collaborativeSetSubblockValue } = useCollaborativeWorkflow()
+
+ const startBlockId = useMemo(() => {
+ if (!blocks || Object.keys(blocks).length === 0) return null
+ const candidate = TriggerUtils.findStartBlock(blocks, 'api')
+ if (!candidate || candidate.path !== StartBlockPath.UNIFIED) return null
+ return candidate.blockId
+ }, [blocks])
+
+ const startBlockInputFormat = useSubBlockStore((state) => {
+ if (!workflowId || !startBlockId) return null
+ const workflowValues = state.workflowValues[workflowId]
+ const fromStore = workflowValues?.[startBlockId]?.inputFormat
+ if (fromStore !== undefined) return fromStore
+ const startBlock = blocks[startBlockId]
+ return startBlock?.subBlocks?.inputFormat?.value ?? null
+ })
+
+ const missingFields = useMemo(() => {
+ if (!startBlockId) return { input: false, data: false, files: false, any: false }
+ const normalizedFields = normalizeInputFormatValue(startBlockInputFormat)
+ const existingNames = new Set(
+ normalizedFields
+ .map((field) => field.name)
+ .filter((n): n is string => typeof n === 'string' && n.trim() !== '')
+ .map((n) => n.trim().toLowerCase())
+ )
+ const missing = {
+ input: !existingNames.has('input'),
+ data: !existingNames.has('data'),
+ files: !existingNames.has('files'),
+ any: false,
+ }
+ missing.any = missing.input || missing.data || missing.files
+ return missing
+ }, [startBlockId, startBlockInputFormat])
+
+ const handleAddA2AInputs = useCallback(() => {
+ if (!startBlockId) return
+
+ const normalizedExisting = normalizeInputFormatValue(startBlockInputFormat)
+ const newFields: InputFormatField[] = []
+
+ // Add input field if missing (for TextPart)
+ if (missingFields.input) {
+ newFields.push({
+ id: crypto.randomUUID(),
+ name: 'input',
+ type: 'string',
+ value: '',
+ collapsed: false,
+ })
+ }
+
+ // Add data field if missing (for DataPart)
+ if (missingFields.data) {
+ newFields.push({
+ id: crypto.randomUUID(),
+ name: 'data',
+ type: 'object',
+ value: '',
+ collapsed: false,
+ })
+ }
+
+ // Add files field if missing (for FilePart)
+ if (missingFields.files) {
+ newFields.push({
+ id: crypto.randomUUID(),
+ name: 'files',
+ type: 'files',
+ value: '',
+ collapsed: false,
+ })
+ }
+
+ if (newFields.length > 0) {
+ const updatedFields = [...newFields, ...normalizedExisting]
+ collaborativeSetSubblockValue(startBlockId, 'inputFormat', updatedFields)
+ logger.info(
+ `Added A2A input fields to Start block: ${newFields.map((f) => f.name).join(', ')}`
+ )
+ }
+ }, [startBlockId, startBlockInputFormat, missingFields, collaborativeSetSubblockValue])
+
+ const [name, setName] = useState('')
+ const [description, setDescription] = useState('')
+ const [authScheme, setAuthScheme] = useState('apiKey')
+ const [pushNotificationsEnabled, setPushNotificationsEnabled] = useState(false)
+ const [skillTags, setSkillTags] = useState([])
+ const [language, setLanguage] = useState('curl')
+ const [useStreamingExample, setUseStreamingExample] = useState(false)
+ const [urlCopied, setUrlCopied] = useState(false)
+ const [codeCopied, setCodeCopied] = useState(false)
+
+ useEffect(() => {
+ if (existingAgent) {
+ setName(existingAgent.name)
+ const savedDesc = existingAgent.description || ''
+ setDescription(isDefaultDescription(savedDesc, workflowName) ? '' : savedDesc)
+ setPushNotificationsEnabled(existingAgent.capabilities?.pushNotifications ?? false)
+ const schemes = existingAgent.authentication?.schemes || []
+ if (schemes.includes('apiKey')) {
+ setAuthScheme('apiKey')
+ } else {
+ setAuthScheme('none')
+ }
+ const skills = existingAgent.skills as Array<{ tags?: string[] }> | undefined
+ const savedTags = skills?.[0]?.tags
+ setSkillTags(savedTags?.length ? savedTags : [])
+ } else {
+ setName(workflowName)
+ setDescription(
+ isDefaultDescription(workflowDescription, workflowName) ? '' : workflowDescription || ''
+ )
+ setAuthScheme('apiKey')
+ setPushNotificationsEnabled(false)
+ setSkillTags([])
+ }
+ }, [existingAgent, workflowName, workflowDescription])
+
+ useEffect(() => {
+ onAgentExistsChange?.(!!existingAgent)
+ }, [existingAgent, onAgentExistsChange])
+
+ useEffect(() => {
+ onPublishedChange?.(existingAgent?.isPublished ?? false)
+ }, [existingAgent?.isPublished, onPublishedChange])
+
+ const hasFormChanges = useMemo(() => {
+ if (!existingAgent) return false
+ const savedSchemes = existingAgent.authentication?.schemes || []
+ const savedAuthScheme = savedSchemes.includes('apiKey') ? 'apiKey' : 'none'
+ const savedDesc = existingAgent.description || ''
+ const normalizedSavedDesc = isDefaultDescription(savedDesc, workflowName) ? '' : savedDesc
+ const skills = existingAgent.skills as Array<{ tags?: string[] }> | undefined
+ const savedTags = skills?.[0]?.tags || []
+ const tagsChanged =
+ skillTags.length !== savedTags.length || skillTags.some((t, i) => t !== savedTags[i])
+ return (
+ name !== existingAgent.name ||
+ description !== normalizedSavedDesc ||
+ pushNotificationsEnabled !== (existingAgent.capabilities?.pushNotifications ?? false) ||
+ authScheme !== savedAuthScheme ||
+ tagsChanged
+ )
+ }, [
+ existingAgent,
+ name,
+ description,
+ pushNotificationsEnabled,
+ authScheme,
+ skillTags,
+ workflowName,
+ ])
+
+ const hasWorkflowChanges = useMemo(() => {
+ if (!existingAgent) return false
+ return !!workflowNeedsRedeployment
+ }, [existingAgent, workflowNeedsRedeployment])
+
+ const needsRepublish = existingAgent && (hasFormChanges || hasWorkflowChanges)
+
+ useEffect(() => {
+ onNeedsRepublishChange?.(!!needsRepublish)
+ }, [needsRepublish, onNeedsRepublishChange])
+
+ const authSchemeOptions: ComboboxOption[] = useMemo(
+ () => [
+ { label: 'API Key', value: 'apiKey' },
+ { label: 'None (Public)', value: 'none' },
+ ],
+ []
+ )
+
+ const canSave = name.trim().length > 0 && description.trim().length > 0
+ useEffect(() => {
+ onCanSaveChange?.(canSave)
+ }, [canSave, onCanSaveChange])
+
+ const isSubmitting =
+ createAgent.isPending ||
+ updateAgent.isPending ||
+ deleteAgent.isPending ||
+ publishAgent.isPending
+
+ useEffect(() => {
+ onSubmittingChange?.(isSubmitting)
+ }, [isSubmitting, onSubmittingChange])
+
+ const handleCreateOrUpdate = useCallback(async () => {
+ const capabilities: AgentCapabilities = {
+ streaming: true,
+ pushNotifications: pushNotificationsEnabled,
+ stateTransitionHistory: true,
+ }
+
+ const authentication: AgentAuthentication = {
+ schemes: authScheme === 'none' ? ['none'] : [authScheme],
+ }
+
+ try {
+ if (existingAgent) {
+ await updateAgent.mutateAsync({
+ agentId: existingAgent.id,
+ name: name.trim(),
+ description: description.trim() || undefined,
+ capabilities,
+ authentication,
+ skillTags,
+ })
+ } else {
+ await createAgent.mutateAsync({
+ workspaceId,
+ workflowId,
+ name: name.trim(),
+ description: description.trim() || undefined,
+ capabilities,
+ authentication,
+ skillTags,
+ })
+ }
+ } catch (error) {
+ logger.error('Failed to save A2A agent:', error)
+ }
+ }, [
+ existingAgent,
+ name,
+ description,
+ pushNotificationsEnabled,
+ authScheme,
+ skillTags,
+ workspaceId,
+ workflowId,
+ createAgent,
+ updateAgent,
+ ])
+
+ const handlePublish = useCallback(async () => {
+ if (!existingAgent) return
+ try {
+ await publishAgent.mutateAsync({
+ agentId: existingAgent.id,
+ workspaceId,
+ action: 'publish',
+ })
+ } catch (error) {
+ logger.error('Failed to publish A2A agent:', error)
+ }
+ }, [existingAgent, workspaceId, publishAgent])
+
+ const handleUnpublish = useCallback(async () => {
+ if (!existingAgent) return
+ try {
+ await publishAgent.mutateAsync({
+ agentId: existingAgent.id,
+ workspaceId,
+ action: 'unpublish',
+ })
+ } catch (error) {
+ logger.error('Failed to unpublish A2A agent:', error)
+ }
+ }, [existingAgent, workspaceId, publishAgent])
+
+ const handleDelete = useCallback(async () => {
+ if (!existingAgent) return
+ try {
+ await deleteAgent.mutateAsync({
+ agentId: existingAgent.id,
+ workspaceId,
+ })
+ setName(workflowName)
+ setDescription(workflowDescription || '')
+ } catch (error) {
+ logger.error('Failed to delete A2A agent:', error)
+ }
+ }, [existingAgent, workspaceId, deleteAgent, workflowName, workflowDescription])
+
+ const handlePublishNewAgent = useCallback(async () => {
+ const capabilities: AgentCapabilities = {
+ streaming: true,
+ pushNotifications: pushNotificationsEnabled,
+ stateTransitionHistory: true,
+ }
+
+ const authentication: AgentAuthentication = {
+ schemes: authScheme === 'none' ? ['none'] : [authScheme],
+ }
+
+ try {
+ if (!isDeployed && onDeployWorkflow) {
+ await onDeployWorkflow()
+ }
+
+ const newAgent = await createAgent.mutateAsync({
+ workspaceId,
+ workflowId,
+ name: name.trim(),
+ description: description.trim() || undefined,
+ capabilities,
+ authentication,
+ skillTags,
+ })
+
+ await publishAgent.mutateAsync({
+ agentId: newAgent.id,
+ workspaceId,
+ action: 'publish',
+ })
+ } catch (error) {
+ logger.error('Failed to publish A2A agent:', error)
+ }
+ }, [
+ name,
+ description,
+ pushNotificationsEnabled,
+ authScheme,
+ skillTags,
+ workspaceId,
+ workflowId,
+ createAgent,
+ publishAgent,
+ isDeployed,
+ onDeployWorkflow,
+ ])
+
+ const handleUpdateAndRepublish = useCallback(async () => {
+ if (!existingAgent) return
+
+ const capabilities: AgentCapabilities = {
+ streaming: true,
+ pushNotifications: pushNotificationsEnabled,
+ stateTransitionHistory: true,
+ }
+
+ const authentication: AgentAuthentication = {
+ schemes: authScheme === 'none' ? ['none'] : [authScheme],
+ }
+
+ try {
+ if ((!isDeployed || workflowNeedsRedeployment) && onDeployWorkflow) {
+ await onDeployWorkflow()
+ }
+
+ await updateAgent.mutateAsync({
+ agentId: existingAgent.id,
+ name: name.trim(),
+ description: description.trim() || undefined,
+ capabilities,
+ authentication,
+ skillTags,
+ })
+
+ await publishAgent.mutateAsync({
+ agentId: existingAgent.id,
+ workspaceId,
+ action: 'publish',
+ })
+ } catch (error) {
+ logger.error('Failed to update and republish A2A agent:', error)
+ }
+ }, [
+ existingAgent,
+ isDeployed,
+ workflowNeedsRedeployment,
+ onDeployWorkflow,
+ name,
+ description,
+ pushNotificationsEnabled,
+ authScheme,
+ skillTags,
+ workspaceId,
+ updateAgent,
+ publishAgent,
+ ])
+
+ const baseUrl = getBaseUrl()
+ const endpoint = existingAgent ? `${baseUrl}/api/a2a/serve/${existingAgent.id}` : null
+
+ const additionalInputFields = useMemo(() => {
+ const allFields = normalizeInputFormatValue(startBlockInputFormat)
+ return allFields.filter(
+ (field): field is InputFormatField & { name: string } =>
+ !!field.name &&
+ field.name.toLowerCase() !== 'input' &&
+ field.name.toLowerCase() !== 'data' &&
+ field.name.toLowerCase() !== 'files'
+ )
+ }, [startBlockInputFormat])
+
+ const getExampleInputData = useCallback((): Record => {
+ const data: Record = {}
+ for (const field of additionalInputFields) {
+ switch (field.type) {
+ case 'string':
+ data[field.name] = 'example'
+ break
+ case 'number':
+ data[field.name] = 42
+ break
+ case 'boolean':
+ data[field.name] = true
+ break
+ case 'object':
+ data[field.name] = { key: 'value' }
+ break
+ case 'array':
+ data[field.name] = [1, 2, 3]
+ break
+ default:
+ data[field.name] = 'example'
+ }
+ }
+ return data
+ }, [additionalInputFields])
+
+ const getJsonRpcPayload = useCallback((): Record => {
+ const inputData = getExampleInputData()
+ const hasAdditionalData = Object.keys(inputData).length > 0
+
+ // Build parts array: TextPart for message text, DataPart for additional fields
+ const parts: Array> = [{ kind: 'text', text: 'Hello, agent!' }]
+ if (hasAdditionalData) {
+ parts.push({ kind: 'data', data: inputData })
+ }
+
+ return {
+ jsonrpc: '2.0',
+ id: '1',
+ method: useStreamingExample ? 'message/stream' : 'message/send',
+ params: {
+ message: {
+ role: 'user',
+ parts,
+ },
+ },
+ }
+ }, [getExampleInputData, useStreamingExample])
+
+ const getCurlCommand = useCallback((): string => {
+ if (!endpoint) return ''
+ const payload = getJsonRpcPayload()
+ const requiresAuth = authScheme !== 'none'
+
+ switch (language) {
+ case 'curl':
+ return requiresAuth
+ ? `curl -X POST \\
+ -H "X-API-Key: $SIM_API_KEY" \\
+ -H "Content-Type: application/json" \\
+ -d '${JSON.stringify(payload)}' \\
+ ${endpoint}`
+ : `curl -X POST \\
+ -H "Content-Type: application/json" \\
+ -d '${JSON.stringify(payload)}' \\
+ ${endpoint}`
+
+ case 'python':
+ return requiresAuth
+ ? `import os
+import requests
+
+response = requests.post(
+ "${endpoint}",
+ headers={
+ "X-API-Key": os.environ.get("SIM_API_KEY"),
+ "Content-Type": "application/json"
+ },
+ json=${JSON.stringify(payload, null, 4).replace(/\n/g, '\n ')}
+)
+
+print(response.json())`
+ : `import requests
+
+response = requests.post(
+ "${endpoint}",
+ headers={"Content-Type": "application/json"},
+ json=${JSON.stringify(payload, null, 4).replace(/\n/g, '\n ')}
+)
+
+print(response.json())`
+
+ case 'javascript':
+ return requiresAuth
+ ? `const response = await fetch("${endpoint}", {
+ method: "POST",
+ headers: {
+ "X-API-Key": process.env.SIM_API_KEY,
+ "Content-Type": "application/json"
+ },
+ body: JSON.stringify(${JSON.stringify(payload)})
+});
+
+const data = await response.json();
+console.log(data);`
+ : `const response = await fetch("${endpoint}", {
+ method: "POST",
+ headers: {"Content-Type": "application/json"},
+ body: JSON.stringify(${JSON.stringify(payload)})
+});
+
+const data = await response.json();
+console.log(data);`
+
+ case 'typescript':
+ return requiresAuth
+ ? `const response = await fetch("${endpoint}", {
+ method: "POST",
+ headers: {
+ "X-API-Key": process.env.SIM_API_KEY,
+ "Content-Type": "application/json"
+ },
+ body: JSON.stringify(${JSON.stringify(payload)})
+});
+
+const data: Record = await response.json();
+console.log(data);`
+ : `const response = await fetch("${endpoint}", {
+ method: "POST",
+ headers: {"Content-Type": "application/json"},
+ body: JSON.stringify(${JSON.stringify(payload)})
+});
+
+const data: Record = await response.json();
+console.log(data);`
+
+ default:
+ return ''
+ }
+ }, [endpoint, language, getJsonRpcPayload, authScheme])
+
+ const handleCopyCommand = useCallback(() => {
+ navigator.clipboard.writeText(getCurlCommand())
+ setCodeCopied(true)
+ setTimeout(() => setCodeCopied(false), 2000)
+ }, [getCurlCommand])
+
+ if (isLoading) {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+ }
+
+ return (
+
+ )
+}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/api/api.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/api/api.tsx
index 0de266a385..e2131a6ced 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/api/api.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/api/api.tsx
@@ -125,12 +125,13 @@ export function ApiDeploy({
${endpoint}`
case 'python':
- return `import requests
+ return `import os
+import requests
response = requests.post(
"${endpoint}",
headers={
- "X-API-Key": SIM_API_KEY,
+ "X-API-Key": os.environ.get("SIM_API_KEY"),
"Content-Type": "application/json"
},
json=${JSON.stringify(payload, null, 4).replace(/\n/g, '\n ')}
@@ -142,7 +143,7 @@ print(response.json())`
return `const response = await fetch("${endpoint}", {
method: "POST",
headers: {
- "X-API-Key": SIM_API_KEY,
+ "X-API-Key": process.env.SIM_API_KEY,
"Content-Type": "application/json"
},
body: JSON.stringify(${JSON.stringify(payload)})
@@ -155,7 +156,7 @@ console.log(data);`
return `const response = await fetch("${endpoint}", {
method: "POST",
headers: {
- "X-API-Key": SIM_API_KEY,
+ "X-API-Key": process.env.SIM_API_KEY,
"Content-Type": "application/json"
},
body: JSON.stringify(${JSON.stringify(payload)})
@@ -183,12 +184,13 @@ console.log(data);`
${endpoint}`
case 'python':
- return `import requests
+ return `import os
+import requests
response = requests.post(
"${endpoint}",
headers={
- "X-API-Key": SIM_API_KEY,
+ "X-API-Key": os.environ.get("SIM_API_KEY"),
"Content-Type": "application/json"
},
json=${JSON.stringify(payload, null, 4).replace(/\n/g, '\n ')},
@@ -203,7 +205,7 @@ for line in response.iter_lines():
return `const response = await fetch("${endpoint}", {
method: "POST",
headers: {
- "X-API-Key": SIM_API_KEY,
+ "X-API-Key": process.env.SIM_API_KEY,
"Content-Type": "application/json"
},
body: JSON.stringify(${JSON.stringify(payload)})
@@ -222,7 +224,7 @@ while (true) {
return `const response = await fetch("${endpoint}", {
method: "POST",
headers: {
- "X-API-Key": SIM_API_KEY,
+ "X-API-Key": process.env.SIM_API_KEY,
"Content-Type": "application/json"
},
body: JSON.stringify(${JSON.stringify(payload)})
@@ -260,12 +262,13 @@ while (true) {
${endpoint}`
case 'python':
- return `import requests
+ return `import os
+import requests
response = requests.post(
"${endpoint}",
headers={
- "X-API-Key": SIM_API_KEY,
+ "X-API-Key": os.environ.get("SIM_API_KEY"),
"Content-Type": "application/json",
"X-Execution-Mode": "async"
},
@@ -279,7 +282,7 @@ print(job) # Contains job_id for status checking`
return `const response = await fetch("${endpoint}", {
method: "POST",
headers: {
- "X-API-Key": SIM_API_KEY,
+ "X-API-Key": process.env.SIM_API_KEY,
"Content-Type": "application/json",
"X-Execution-Mode": "async"
},
@@ -293,7 +296,7 @@ console.log(job); // Contains job_id for status checking`
return `const response = await fetch("${endpoint}", {
method: "POST",
headers: {
- "X-API-Key": SIM_API_KEY,
+ "X-API-Key": process.env.SIM_API_KEY,
"Content-Type": "application/json",
"X-Execution-Mode": "async"
},
@@ -314,11 +317,12 @@ console.log(job); // Contains job_id for status checking`
${baseUrl}/api/jobs/JOB_ID_FROM_EXECUTION`
case 'python':
- return `import requests
+ return `import os
+import requests
response = requests.get(
"${baseUrl}/api/jobs/JOB_ID_FROM_EXECUTION",
- headers={"X-API-Key": SIM_API_KEY}
+ headers={"X-API-Key": os.environ.get("SIM_API_KEY")}
)
status = response.json()
@@ -328,7 +332,7 @@ print(status)`
return `const response = await fetch(
"${baseUrl}/api/jobs/JOB_ID_FROM_EXECUTION",
{
- headers: { "X-API-Key": SIM_API_KEY }
+ headers: { "X-API-Key": process.env.SIM_API_KEY }
}
);
@@ -339,7 +343,7 @@ console.log(status);`
return `const response = await fetch(
"${baseUrl}/api/jobs/JOB_ID_FROM_EXECUTION",
{
- headers: { "X-API-Key": SIM_API_KEY }
+ headers: { "X-API-Key": process.env.SIM_API_KEY }
}
);
@@ -357,11 +361,12 @@ console.log(status);`
${baseUrl}/api/users/me/usage-limits`
case 'python':
- return `import requests
+ return `import os
+import requests
response = requests.get(
"${baseUrl}/api/users/me/usage-limits",
- headers={"X-API-Key": SIM_API_KEY}
+ headers={"X-API-Key": os.environ.get("SIM_API_KEY")}
)
limits = response.json()
@@ -371,7 +376,7 @@ print(limits)`
return `const response = await fetch(
"${baseUrl}/api/users/me/usage-limits",
{
- headers: { "X-API-Key": SIM_API_KEY }
+ headers: { "X-API-Key": process.env.SIM_API_KEY }
}
);
@@ -382,7 +387,7 @@ console.log(limits);`
return `const response = await fetch(
"${baseUrl}/api/users/me/usage-limits",
{
- headers: { "X-API-Key": SIM_API_KEY }
+ headers: { "X-API-Key": process.env.SIM_API_KEY }
}
);
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/chat/chat.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/chat/chat.tsx
index c150ad3ab6..e254c7ce1c 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/chat/chat.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/chat/chat.tsx
@@ -23,7 +23,7 @@ import { Alert, AlertDescription, Skeleton } from '@/components/ui'
import { getEnv, isTruthy } from '@/lib/core/config/env'
import { generatePassword } from '@/lib/core/security/encryption'
import { cn } from '@/lib/core/utils/cn'
-import { getEmailDomain } from '@/lib/core/utils/urls'
+import { getBaseUrl, getEmailDomain } from '@/lib/core/utils/urls'
import { quickValidateEmail } from '@/lib/messaging/email/validation'
import { OutputSelect } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/chat/components/output-select/output-select'
import {
@@ -493,7 +493,7 @@ function IdentifierInput({
onChange(lowercaseValue)
}
- const fullUrl = `${getEnv('NEXT_PUBLIC_APP_URL')}/chat/${value}`
+ const fullUrl = `${getBaseUrl()}/chat/${value}`
const displayUrl = fullUrl.replace(/^https?:\/\//, '')
return (
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/form/form.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/form/form.tsx
index 614950c91f..3f0fd9ea50 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/form/form.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/form/form.tsx
@@ -14,7 +14,6 @@ import {
Tooltip,
} from '@/components/emcn'
import { Skeleton } from '@/components/ui'
-import { getEnv } from '@/lib/core/config/env'
import { isDev } from '@/lib/core/config/feature-flags'
import { cn } from '@/lib/core/utils/cn'
import { getBaseUrl, getEmailDomain } from '@/lib/core/utils/urls'
@@ -392,7 +391,7 @@ export function FormDeploy({
)
}
- const fullUrl = `${getEnv('NEXT_PUBLIC_APP_URL')}/form/${identifier}`
+ const fullUrl = `${getBaseUrl()}/form/${identifier}`
const displayUrl = fullUrl.replace(/^https?:\/\//, '')
return (
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/mcp/mcp.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/mcp/mcp.tsx
index fb4a0aecb3..b736760fea 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/mcp/mcp.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/mcp/mcp.tsx
@@ -513,25 +513,31 @@ export function McpDeploy({
{inputFormat.map((field) => (
-
-
{field.name}
-
- {field.type}
-
+
+
+
+ {field.name}
+
+ {field.type}
+
+
+
-
- setParameterDescriptions((prev) => ({
- ...prev,
- [field.name]: e.target.value,
- }))
- }
- placeholder='Description'
- className='mt-[6px] h-[28px] text-[12px]'
- />
))}
@@ -551,7 +557,6 @@ export function McpDeploy({
searchable
searchPlaceholder='Search servers...'
disabled={!toolName.trim() || isPending}
- isLoading={isPending}
overlayContent={
{selectedServersLabel}
}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/template/template.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/template/template.tsx
index dd7cdeee9f..5d541a31fc 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/template/template.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/template/template.tsx
@@ -12,9 +12,10 @@ import {
ModalContent,
ModalFooter,
ModalHeader,
+ TagInput,
Textarea,
} from '@/components/emcn'
-import { Skeleton, TagInput } from '@/components/ui'
+import { Skeleton } from '@/components/ui'
import { useSession } from '@/lib/auth/auth-client'
import { cn } from '@/lib/core/utils/cn'
import { captureAndUploadOGImage, OG_IMAGE_HEIGHT, OG_IMAGE_WIDTH } from '@/lib/og'
@@ -404,10 +405,24 @@ export function TemplateDeploy({
Tags
updateField('tags', tags)}
+ items={formData.tags.map((tag) => ({ value: tag, isValid: true }))}
+ onAdd={(value) => {
+ if (!formData.tags.includes(value) && formData.tags.length < 10) {
+ updateField('tags', [...formData.tags, value])
+ return true
+ }
+ return false
+ }}
+ onRemove={(_value, index) => {
+ updateField(
+ 'tags',
+ formData.tags.filter((_, i) => i !== index)
+ )
+ }}
placeholder='Dev, Agents, Research, etc.'
- maxTags={10}
+ placeholderWithTags='Add another'
+ tagVariant='secondary'
+ triggerKeys={['Enter', ',']}
disabled={isSubmitting}
/>
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/deploy-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/deploy-modal.tsx
index 1ca73f5fb3..c0e6756712 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/deploy-modal.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/deploy-modal.tsx
@@ -15,7 +15,7 @@ import {
ModalTabsList,
ModalTabsTrigger,
} from '@/components/emcn'
-import { getEnv } from '@/lib/core/config/env'
+import { getBaseUrl } from '@/lib/core/utils/urls'
import { getInputFormatExample as getInputFormatExampleUtil } from '@/lib/workflows/operations/deployment-utils'
import type { WorkflowDeploymentVersionResponse } from '@/lib/workflows/persistence/utils'
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
@@ -27,6 +27,7 @@ import { useSettingsModalStore } from '@/stores/modals/settings/store'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
import type { WorkflowState } from '@/stores/workflows/workflow/types'
+import { A2aDeploy } from './components/a2a/a2a'
import { ApiDeploy } from './components/api/api'
import { ChatDeploy, type ExistingChat } from './components/chat/chat'
import { GeneralDeploy } from './components/general/general'
@@ -55,7 +56,7 @@ interface WorkflowDeploymentInfo {
needsRedeployment: boolean
}
-type TabView = 'general' | 'api' | 'chat' | 'template' | 'mcp' | 'form'
+type TabView = 'general' | 'api' | 'chat' | 'template' | 'mcp' | 'form' | 'a2a'
export function DeployModal({
open,
@@ -96,6 +97,12 @@ export function DeployModal({
const [mcpToolSubmitting, setMcpToolSubmitting] = useState(false)
const [mcpToolCanSave, setMcpToolCanSave] = useState(false)
const [hasMcpServers, setHasMcpServers] = useState(false)
+ const [a2aSubmitting, setA2aSubmitting] = useState(false)
+ const [a2aCanSave, setA2aCanSave] = useState(false)
+ const [hasA2aAgent, setHasA2aAgent] = useState(false)
+ const [isA2aPublished, setIsA2aPublished] = useState(false)
+ const [a2aNeedsRepublish, setA2aNeedsRepublish] = useState(false)
+ const [showA2aDeleteConfirm, setShowA2aDeleteConfirm] = useState(false)
const [hasExistingTemplate, setHasExistingTemplate] = useState(false)
const [templateStatus, setTemplateStatus] = useState<{
status: 'pending' | 'approved' | 'rejected' | null
@@ -209,7 +216,7 @@ export function DeployModal({
}
const data = await response.json()
- const endpoint = `${getEnv('NEXT_PUBLIC_APP_URL')}/api/workflows/${workflowId}/execute`
+ const endpoint = `${getBaseUrl()}/api/workflows/${workflowId}/execute`
const inputFormatExample = getInputFormatExample(selectedStreamingOutputs.length > 0)
const placeholderKey = workflowWorkspaceId ? 'YOUR_WORKSPACE_API_KEY' : 'YOUR_API_KEY'
@@ -270,7 +277,7 @@ export function DeployModal({
const deploymentInfoResponse = await fetch(`/api/workflows/${workflowId}/deploy`)
if (deploymentInfoResponse.ok) {
const deploymentData = await deploymentInfoResponse.json()
- const apiEndpoint = `${getEnv('NEXT_PUBLIC_APP_URL')}/api/workflows/${workflowId}/execute`
+ const apiEndpoint = `${getBaseUrl()}/api/workflows/${workflowId}/execute`
const inputFormatExample = getInputFormatExample(selectedStreamingOutputs.length > 0)
const placeholderKey = getApiHeaderPlaceholder()
@@ -368,7 +375,6 @@ export function DeployModal({
async (version: number) => {
if (!workflowId) return
- // Optimistically update versions to show the new active version immediately
const previousVersions = [...versions]
setVersions((prev) =>
prev.map((v) => ({
@@ -402,14 +408,13 @@ export function DeployModal({
setDeploymentStatus(workflowId, true, deployedAtTime, apiKeyLabel)
- // Refresh deployed state in background (no loading flash)
refetchDeployedState()
fetchVersions()
const deploymentInfoResponse = await fetch(`/api/workflows/${workflowId}/deploy`)
if (deploymentInfoResponse.ok) {
const deploymentData = await deploymentInfoResponse.json()
- const apiEndpoint = `${getEnv('NEXT_PUBLIC_APP_URL')}/api/workflows/${workflowId}/execute`
+ const apiEndpoint = `${getBaseUrl()}/api/workflows/${workflowId}/execute`
const inputFormatExample = getInputFormatExample(selectedStreamingOutputs.length > 0)
const placeholderKey = getApiHeaderPlaceholder()
@@ -423,7 +428,6 @@ export function DeployModal({
})
}
} catch (error) {
- // Rollback optimistic update on error
setVersions(previousVersions)
throw error
}
@@ -526,7 +530,7 @@ export function DeployModal({
const deploymentInfoResponse = await fetch(`/api/workflows/${workflowId}/deploy`)
if (deploymentInfoResponse.ok) {
const deploymentData = await deploymentInfoResponse.json()
- const apiEndpoint = `${getEnv('NEXT_PUBLIC_APP_URL')}/api/workflows/${workflowId}/execute`
+ const apiEndpoint = `${getBaseUrl()}/api/workflows/${workflowId}/execute`
const inputFormatExample = getInputFormatExample(selectedStreamingOutputs.length > 0)
const placeholderKey = getApiHeaderPlaceholder()
@@ -578,6 +582,48 @@ export function DeployModal({
form?.requestSubmit()
}, [])
+ const handleA2aFormSubmit = useCallback(() => {
+ const form = document.getElementById('a2a-deploy-form') as HTMLFormElement
+ form?.requestSubmit()
+ }, [])
+
+ const handleA2aPublish = useCallback(() => {
+ const form = document.getElementById('a2a-deploy-form')
+ const publishTrigger = form?.querySelector('[data-a2a-publish-trigger]') as HTMLButtonElement
+ publishTrigger?.click()
+ }, [])
+
+ const handleA2aUnpublish = useCallback(() => {
+ const form = document.getElementById('a2a-deploy-form')
+ const unpublishTrigger = form?.querySelector(
+ '[data-a2a-unpublish-trigger]'
+ ) as HTMLButtonElement
+ unpublishTrigger?.click()
+ }, [])
+
+ const handleA2aPublishNew = useCallback(() => {
+ const form = document.getElementById('a2a-deploy-form')
+ const publishNewTrigger = form?.querySelector(
+ '[data-a2a-publish-new-trigger]'
+ ) as HTMLButtonElement
+ publishNewTrigger?.click()
+ }, [])
+
+ const handleA2aUpdateRepublish = useCallback(() => {
+ const form = document.getElementById('a2a-deploy-form')
+ const updateRepublishTrigger = form?.querySelector(
+ '[data-a2a-update-republish-trigger]'
+ ) as HTMLButtonElement
+ updateRepublishTrigger?.click()
+ }, [])
+
+ const handleA2aDelete = useCallback(() => {
+ const form = document.getElementById('a2a-deploy-form')
+ const deleteTrigger = form?.querySelector('[data-a2a-delete-trigger]') as HTMLButtonElement
+ deleteTrigger?.click()
+ setShowA2aDeleteConfirm(false)
+ }, [])
+
const handleTemplateDelete = useCallback(() => {
const form = document.getElementById('template-deploy-form')
const deleteTrigger = form?.querySelector('[data-template-delete-trigger]') as HTMLButtonElement
@@ -610,6 +656,7 @@ export function DeployModal({
General
API
MCP
+ A2A
Chat
{/* Form */}
Template
@@ -700,6 +747,24 @@ export function DeployModal({
/>
)}
+
+
+ {workflowId && (
+
+ )}
+
@@ -715,19 +780,23 @@ export function DeployModal({
/>
)}
{activeTab === 'api' && (
-
- setIsCreateKeyModalOpen(true)}
- disabled={createButtonDisabled}
- >
- Generate API Key
-
+
+
+
+ setIsCreateKeyModalOpen(true)}
+ disabled={createButtonDisabled}
+ >
+ Generate API Key
+
+
)}
{activeTab === 'chat' && (
-
-
+
+
+
{chatExists && (
)}
{activeTab === 'mcp' && isDeployed && hasMcpServers && (
-
-
+
+
+
)}
{activeTab === 'template' && (
-
- {hasExistingTemplate && templateStatus && (
+
+ {hasExistingTemplate && templateStatus ? (
+ ) : (
+
)}
-
+
{hasExistingTemplate && (
)}
{/* {activeTab === 'form' && (
-
-
+
+
+
{formExists && (
)} */}
+ {activeTab === 'a2a' && (
+
+ {/* Status badge on left */}
+ {hasA2aAgent ? (
+ isA2aPublished ? (
+
+ {a2aNeedsRepublish ? 'Update deployment' : 'Live'}
+
+ ) : (
+
+ Unpublished
+
+ )
+ ) : (
+
+ )}
+
+ {/* No agent exists: Show "Publish Agent" button */}
+ {!hasA2aAgent && (
+
+ {a2aSubmitting ? 'Publishing...' : 'Publish Agent'}
+
+ )}
+
+ {/* Agent exists and published: Show Unpublish and Update */}
+ {hasA2aAgent && isA2aPublished && (
+ <>
+
+ Unpublish
+
+
+ {a2aSubmitting ? 'Updating...' : 'Update'}
+
+ >
+ )}
+
+ {/* Agent exists but unpublished: Show Delete and Publish */}
+ {hasA2aAgent && !isA2aPublished && (
+ <>
+ setShowA2aDeleteConfirm(true)}
+ disabled={a2aSubmitting}
+ >
+ Delete
+
+
+ {a2aSubmitting ? 'Publishing...' : 'Publish'}
+
+ >
+ )}
+
+
+ )}
@@ -882,6 +1028,32 @@ export function DeployModal({
+
+
+ Delete A2A Agent
+
+
+ Are you sure you want to delete this agent?{' '}
+
+ This will permanently remove the agent configuration.
+
+
+
+
+ setShowA2aDeleteConfirm(false)}
+ disabled={a2aSubmitting}
+ >
+ Cancel
+
+
+ {a2aSubmitting ? 'Deleting...' : 'Delete'}
+
+
+
+
+
-
- {isSubmitting ? 'Deploying...' : 'Deploy'}
-
+
+
+
+
+ {isSubmitting ? 'Deploying...' : 'Deploy'}
+
+
)
}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx
index 2ac4f3cb72..f4e2b54883 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx
@@ -2319,6 +2319,8 @@ const WorkflowContent = React.memo(() => {
/**
* Handles connection drag end. Detects if the edge was dropped over a block
* and automatically creates a connection to that block's target handle.
+ * Only creates a connection if ReactFlow didn't already handle it (e.g., when
+ * dropping on the block body instead of a handle).
*/
const onConnectEnd = useCallback(
(event: MouseEvent | TouchEvent) => {
@@ -2340,14 +2342,25 @@ const WorkflowContent = React.memo(() => {
// Find node under cursor
const targetNode = findNodeAtPosition(flowPosition)
- // Create connection if valid target found
+ // Create connection if valid target found AND edge doesn't already exist
+ // ReactFlow's onConnect fires first when dropping on a handle, so we check
+ // if that connection already exists to avoid creating duplicates.
+ // IMPORTANT: We must read directly from the store (not React state) because
+ // the store update from ReactFlow's onConnect may not have triggered a
+ // React re-render yet when this callback runs (typically 1-2ms later).
if (targetNode && targetNode.id !== source.nodeId) {
- onConnect({
- source: source.nodeId,
- sourceHandle: source.handleId,
- target: targetNode.id,
- targetHandle: 'target',
- })
+ const currentEdges = useWorkflowStore.getState().edges
+ const edgeAlreadyExists = currentEdges.some(
+ (e) => e.source === source.nodeId && e.target === targetNode.id
+ )
+ if (!edgeAlreadyExists) {
+ onConnect({
+ source: source.nodeId,
+ sourceHandle: source.handleId,
+ target: targetNode.id,
+ targetHandle: 'target',
+ })
+ }
}
connectionSourceRef.current = null
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/folder-item/folder-item.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/folder-item/folder-item.tsx
index b105472bee..7bef1cee4b 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/folder-item/folder-item.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/folder-item/folder-item.tsx
@@ -72,7 +72,6 @@ export function FolderItem({ folder, level, hoverHandlers }: FolderItemProps) {
})
const { isExporting, hasWorkflows, handleExportFolder } = useExportFolder({
- workspaceId,
folderId: folder.id,
})
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/workflow-item/workflow-item.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/workflow-item/workflow-item.tsx
index eaad0371a4..ec12c8b294 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/workflow-item/workflow-item.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/workflow-item/workflow-item.tsx
@@ -80,7 +80,7 @@ export function WorkflowItem({ workflow, active, level, onWorkflowClick }: Workf
const { handleDuplicateWorkflow: duplicateWorkflow } = useDuplicateWorkflow({ workspaceId })
- const { handleExportWorkflow: exportWorkflow } = useExportWorkflow({ workspaceId })
+ const { handleExportWorkflow: exportWorkflow } = useExportWorkflow()
const handleDuplicateWorkflow = useCallback(() => {
const workflowIds = capturedSelectionRef.current?.workflowIds || []
if (workflowIds.length === 0) return
diff --git a/apps/sim/app/workspace/[workspaceId]/w/hooks/use-export-folder.ts b/apps/sim/app/workspace/[workspaceId]/w/hooks/use-export-folder.ts
index a35da0616f..402b13535f 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/hooks/use-export-folder.ts
+++ b/apps/sim/app/workspace/[workspaceId]/w/hooks/use-export-folder.ts
@@ -1,20 +1,21 @@
import { useCallback, useMemo, useState } from 'react'
import { createLogger } from '@sim/logger'
-import JSZip from 'jszip'
-import { sanitizeForExport } from '@/lib/workflows/sanitization/json-sanitizer'
+import {
+ downloadFile,
+ exportFolderToZip,
+ type FolderExportData,
+ fetchWorkflowForExport,
+ sanitizePathSegment,
+ type WorkflowExportData,
+} from '@/lib/workflows/operations/import-export'
import { useFolderStore } from '@/stores/folders/store'
import type { WorkflowFolder } from '@/stores/folders/types'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import type { WorkflowMetadata } from '@/stores/workflows/registry/types'
-import type { Variable } from '@/stores/workflows/workflow/types'
const logger = createLogger('useExportFolder')
interface UseExportFolderProps {
- /**
- * Current workspace ID
- */
- workspaceId: string
/**
* The folder ID to export
*/
@@ -25,85 +26,80 @@ interface UseExportFolderProps {
onSuccess?: () => void
}
+interface CollectedWorkflow {
+ id: string
+ folderId: string | null
+}
+
/**
- * Recursively collects all workflow IDs within a folder and its subfolders.
- *
- * @param folderId - The folder ID to collect workflows from
- * @param workflows - All workflows in the workspace
- * @param folders - All folders in the workspace
- * @returns Array of workflow IDs
+ * Recursively collects all workflows within a folder and its subfolders.
*/
function collectWorkflowsInFolder(
folderId: string,
workflows: Record,
folders: Record
-): string[] {
- const workflowIds: string[] = []
+): CollectedWorkflow[] {
+ const collectedWorkflows: CollectedWorkflow[] = []
for (const workflow of Object.values(workflows)) {
if (workflow.folderId === folderId) {
- workflowIds.push(workflow.id)
+ collectedWorkflows.push({ id: workflow.id, folderId: workflow.folderId ?? null })
}
}
for (const folder of Object.values(folders)) {
if (folder.parentId === folderId) {
- const childWorkflowIds = collectWorkflowsInFolder(folder.id, workflows, folders)
- workflowIds.push(...childWorkflowIds)
+ const childWorkflows = collectWorkflowsInFolder(folder.id, workflows, folders)
+ collectedWorkflows.push(...childWorkflows)
+ }
+ }
+
+ return collectedWorkflows
+}
+
+/**
+ * Collects all subfolders recursively under a root folder.
+ * Returns folders with parentId adjusted so direct children of rootFolderId have parentId: null.
+ */
+function collectSubfolders(
+ rootFolderId: string,
+ folders: Record
+): FolderExportData[] {
+ const subfolders: FolderExportData[] = []
+
+ function collect(parentId: string) {
+ for (const folder of Object.values(folders)) {
+ if (folder.parentId === parentId) {
+ subfolders.push({
+ id: folder.id,
+ name: folder.name,
+ // Direct children of root become top-level in export (parentId: null)
+ parentId: folder.parentId === rootFolderId ? null : folder.parentId,
+ })
+ collect(folder.id)
+ }
}
}
- return workflowIds
+ collect(rootFolderId)
+ return subfolders
}
/**
* Hook for managing folder export to ZIP.
- *
- * @param props - Hook configuration
- * @returns Export folder handlers and state
*/
-export function useExportFolder({ workspaceId, folderId, onSuccess }: UseExportFolderProps) {
+export function useExportFolder({ folderId, onSuccess }: UseExportFolderProps) {
const { workflows } = useWorkflowRegistry()
const { folders } = useFolderStore()
const [isExporting, setIsExporting] = useState(false)
- /**
- * Check if the folder has any workflows (recursively)
- */
const hasWorkflows = useMemo(() => {
if (!folderId) return false
return collectWorkflowsInFolder(folderId, workflows, folders).length > 0
}, [folderId, workflows, folders])
- /**
- * Download file helper
- */
- const downloadFile = (content: Blob, filename: string, mimeType = 'application/zip') => {
- try {
- const blob = content instanceof Blob ? content : new Blob([content], { type: mimeType })
- const url = URL.createObjectURL(blob)
- const a = document.createElement('a')
- a.href = url
- a.download = filename
- document.body.appendChild(a)
- a.click()
- document.body.removeChild(a)
- URL.revokeObjectURL(url)
- } catch (error) {
- logger.error('Failed to download file:', error)
- }
- }
-
- /**
- * Export all workflows in the folder (including nested subfolders) to ZIP
- */
const handleExportFolder = useCallback(async () => {
- if (isExporting) {
- return
- }
-
- if (!folderId) {
- logger.warn('No folder ID provided for export')
+ if (isExporting || !folderId) {
return
}
@@ -117,98 +113,57 @@ export function useExportFolder({ workspaceId, folderId, onSuccess }: UseExportF
return
}
- const workflowIdsToExport = collectWorkflowsInFolder(folderId, workflows, folderStore.folders)
+ const workflowsToExport = collectWorkflowsInFolder(folderId, workflows, folderStore.folders)
- if (workflowIdsToExport.length === 0) {
+ if (workflowsToExport.length === 0) {
logger.warn('No workflows found in folder to export', { folderId, folderName: folder.name })
return
}
+ const subfolders = collectSubfolders(folderId, folderStore.folders)
+
logger.info('Starting folder export', {
folderId,
folderName: folder.name,
- workflowCount: workflowIdsToExport.length,
+ workflowCount: workflowsToExport.length,
+ subfolderCount: subfolders.length,
})
- const exportedWorkflows: Array<{ name: string; content: string }> = []
-
- for (const workflowId of workflowIdsToExport) {
- try {
- const workflow = workflows[workflowId]
- if (!workflow) {
- logger.warn(`Workflow ${workflowId} not found in registry`)
- continue
- }
-
- const workflowResponse = await fetch(`/api/workflows/${workflowId}`)
- if (!workflowResponse.ok) {
- logger.error(`Failed to fetch workflow ${workflowId}`)
- continue
- }
-
- const { data: workflowData } = await workflowResponse.json()
- if (!workflowData?.state) {
- logger.warn(`Workflow ${workflowId} has no state`)
- continue
- }
-
- const variablesResponse = await fetch(`/api/workflows/${workflowId}/variables`)
- let workflowVariables: Record | undefined
- if (variablesResponse.ok) {
- const variablesData = await variablesResponse.json()
- workflowVariables = variablesData?.data
- }
-
- const workflowState = {
- ...workflowData.state,
- metadata: {
- name: workflow.name,
- description: workflow.description,
- color: workflow.color,
- exportedAt: new Date().toISOString(),
- },
- variables: workflowVariables,
- }
-
- const exportState = sanitizeForExport(workflowState)
- const jsonString = JSON.stringify(exportState, null, 2)
-
- exportedWorkflows.push({
- name: workflow.name,
- content: jsonString,
- })
-
- logger.info(`Workflow ${workflowId} exported successfully`)
- } catch (error) {
- logger.error(`Failed to export workflow ${workflowId}:`, error)
+ const workflowExportData: WorkflowExportData[] = []
+
+ for (const collectedWorkflow of workflowsToExport) {
+ const workflowMeta = workflows[collectedWorkflow.id]
+ if (!workflowMeta) {
+ logger.warn(`Workflow ${collectedWorkflow.id} not found in registry`)
+ continue
+ }
+
+ const remappedFolderId =
+ collectedWorkflow.folderId === folderId ? null : collectedWorkflow.folderId
+
+ const exportData = await fetchWorkflowForExport(collectedWorkflow.id, {
+ name: workflowMeta.name,
+ description: workflowMeta.description,
+ color: workflowMeta.color,
+ folderId: remappedFolderId,
+ })
+
+ if (exportData) {
+ workflowExportData.push(exportData)
+ logger.info(`Workflow ${collectedWorkflow.id} prepared for export`)
}
}
- if (exportedWorkflows.length === 0) {
- logger.warn('No workflows were successfully exported from folder', {
+ if (workflowExportData.length === 0) {
+ logger.warn('No workflows were successfully prepared for export', {
folderId,
folderName: folder.name,
})
return
}
- const zip = new JSZip()
- const seenFilenames = new Set()
-
- for (const exportedWorkflow of exportedWorkflows) {
- const baseName = exportedWorkflow.name.replace(/[^a-z0-9]/gi, '-')
- let filename = `${baseName}.json`
- let counter = 1
- while (seenFilenames.has(filename.toLowerCase())) {
- filename = `${baseName}-${counter}.json`
- counter++
- }
- seenFilenames.add(filename.toLowerCase())
- zip.file(filename, exportedWorkflow.content)
- }
-
- const zipBlob = await zip.generateAsync({ type: 'blob' })
- const zipFilename = `${folder.name.replace(/[^a-z0-9]/gi, '-')}-export.zip`
+ const zipBlob = await exportFolderToZip(folder.name, workflowExportData, subfolders)
+ const zipFilename = `${sanitizePathSegment(folder.name)}-export.zip`
downloadFile(zipBlob, zipFilename, 'application/zip')
const { clearSelection } = useFolderStore.getState()
@@ -217,7 +172,8 @@ export function useExportFolder({ workspaceId, folderId, onSuccess }: UseExportF
logger.info('Folder exported successfully', {
folderId,
folderName: folder.name,
- workflowCount: exportedWorkflows.length,
+ workflowCount: workflowExportData.length,
+ subfolderCount: subfolders.length,
})
onSuccess?.()
diff --git a/apps/sim/app/workspace/[workspaceId]/w/hooks/use-export-workflow.ts b/apps/sim/app/workspace/[workspaceId]/w/hooks/use-export-workflow.ts
index dbfa1b8524..e3fa6507ab 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/hooks/use-export-workflow.ts
+++ b/apps/sim/app/workspace/[workspaceId]/w/hooks/use-export-workflow.ts
@@ -1,18 +1,18 @@
import { useCallback, useState } from 'react'
import { createLogger } from '@sim/logger'
-import JSZip from 'jszip'
-import { sanitizeForExport } from '@/lib/workflows/sanitization/json-sanitizer'
+import {
+ downloadFile,
+ exportWorkflowsToZip,
+ exportWorkflowToJson,
+ fetchWorkflowForExport,
+ sanitizePathSegment,
+} from '@/lib/workflows/operations/import-export'
import { useFolderStore } from '@/stores/folders/store'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
-import type { Variable } from '@/stores/workflows/workflow/types'
const logger = createLogger('useExportWorkflow')
interface UseExportWorkflowProps {
- /**
- * Current workspace ID
- */
- workspaceId: string
/**
* Optional callback after successful export
*/
@@ -20,44 +20,16 @@ interface UseExportWorkflowProps {
}
/**
- * Hook for managing workflow export to JSON.
- *
- * @param props - Hook configuration
- * @returns Export workflow handlers and state
+ * Hook for managing workflow export to JSON or ZIP.
*/
-export function useExportWorkflow({ workspaceId, onSuccess }: UseExportWorkflowProps) {
+export function useExportWorkflow({ onSuccess }: UseExportWorkflowProps = {}) {
const { workflows } = useWorkflowRegistry()
const [isExporting, setIsExporting] = useState(false)
- /**
- * Download file helper
- */
- const downloadFile = (
- content: Blob | string,
- filename: string,
- mimeType = 'application/json'
- ) => {
- try {
- const blob = content instanceof Blob ? content : new Blob([content], { type: mimeType })
- const url = URL.createObjectURL(blob)
- const a = document.createElement('a')
- a.href = url
- a.download = filename
- document.body.appendChild(a)
- a.click()
- document.body.removeChild(a)
- URL.revokeObjectURL(url)
- } catch (error) {
- logger.error('Failed to download file:', error)
- }
- }
-
/**
* Export the workflow(s) to JSON or ZIP
* - Single workflow: exports as JSON file
* - Multiple workflows: exports as ZIP file containing all JSON files
- * Fetches workflow data from API to support bulk export of non-active workflows
- * @param workflowIds - The workflow ID(s) to export
*/
const handleExportWorkflow = useCallback(
async (workflowIds: string | string[]) => {
@@ -78,85 +50,39 @@ export function useExportWorkflow({ workspaceId, onSuccess }: UseExportWorkflowP
count: workflowIdsToExport.length,
})
- const exportedWorkflows: Array<{ name: string; content: string }> = []
+ const exportedWorkflows = []
for (const workflowId of workflowIdsToExport) {
- try {
- const workflow = workflows[workflowId]
- if (!workflow) {
- logger.warn(`Workflow ${workflowId} not found in registry`)
- continue
- }
-
- const workflowResponse = await fetch(`/api/workflows/${workflowId}`)
- if (!workflowResponse.ok) {
- logger.error(`Failed to fetch workflow ${workflowId}`)
- continue
- }
-
- const { data: workflowData } = await workflowResponse.json()
- if (!workflowData?.state) {
- logger.warn(`Workflow ${workflowId} has no state`)
- continue
- }
-
- const variablesResponse = await fetch(`/api/workflows/${workflowId}/variables`)
- let workflowVariables: Record | undefined
- if (variablesResponse.ok) {
- const variablesData = await variablesResponse.json()
- workflowVariables = variablesData?.data
- }
-
- const workflowState = {
- ...workflowData.state,
- metadata: {
- name: workflow.name,
- description: workflow.description,
- color: workflow.color,
- exportedAt: new Date().toISOString(),
- },
- variables: workflowVariables,
- }
-
- const exportState = sanitizeForExport(workflowState)
- const jsonString = JSON.stringify(exportState, null, 2)
-
- exportedWorkflows.push({
- name: workflow.name,
- content: jsonString,
- })
-
- logger.info(`Workflow ${workflowId} exported successfully`)
- } catch (error) {
- logger.error(`Failed to export workflow ${workflowId}:`, error)
+ const workflowMeta = workflows[workflowId]
+ if (!workflowMeta) {
+ logger.warn(`Workflow ${workflowId} not found in registry`)
+ continue
+ }
+
+ const exportData = await fetchWorkflowForExport(workflowId, {
+ name: workflowMeta.name,
+ description: workflowMeta.description,
+ color: workflowMeta.color,
+ folderId: workflowMeta.folderId,
+ })
+
+ if (exportData) {
+ exportedWorkflows.push(exportData)
+ logger.info(`Workflow ${workflowId} prepared for export`)
}
}
if (exportedWorkflows.length === 0) {
- logger.warn('No workflows were successfully exported')
+ logger.warn('No workflows were successfully prepared for export')
return
}
if (exportedWorkflows.length === 1) {
- const filename = `${exportedWorkflows[0].name.replace(/[^a-z0-9]/gi, '-')}.json`
- downloadFile(exportedWorkflows[0].content, filename, 'application/json')
+ const jsonContent = exportWorkflowToJson(exportedWorkflows[0])
+ const filename = `${sanitizePathSegment(exportedWorkflows[0].workflow.name)}.json`
+ downloadFile(jsonContent, filename, 'application/json')
} else {
- const zip = new JSZip()
- const seenFilenames = new Set()
-
- for (const exportedWorkflow of exportedWorkflows) {
- const baseName = exportedWorkflow.name.replace(/[^a-z0-9]/gi, '-')
- let filename = `${baseName}.json`
- let counter = 1
- while (seenFilenames.has(filename.toLowerCase())) {
- filename = `${baseName}-${counter}.json`
- counter++
- }
- seenFilenames.add(filename.toLowerCase())
- zip.file(filename, exportedWorkflow.content)
- }
-
- const zipBlob = await zip.generateAsync({ type: 'blob' })
+ const zipBlob = await exportWorkflowsToZip(exportedWorkflows)
const zipFilename = `workflows-export-${Date.now()}.zip`
downloadFile(zipBlob, zipFilename, 'application/zip')
}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/hooks/use-export-workspace.ts b/apps/sim/app/workspace/[workspaceId]/w/hooks/use-export-workspace.ts
index af663c92d0..1f855d99c1 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/hooks/use-export-workspace.ts
+++ b/apps/sim/app/workspace/[workspaceId]/w/hooks/use-export-workspace.ts
@@ -1,11 +1,13 @@
import { useCallback, useState } from 'react'
import { createLogger } from '@sim/logger'
import {
+ downloadFile,
exportWorkspaceToZip,
type FolderExportData,
+ fetchWorkflowForExport,
+ sanitizePathSegment,
type WorkflowExportData,
} from '@/lib/workflows/operations/import-export'
-import type { Variable } from '@/stores/workflows/workflow/types'
const logger = createLogger('useExportWorkspace')
@@ -18,24 +20,10 @@ interface UseExportWorkspaceProps {
/**
* Hook for managing workspace export to ZIP.
- *
- * Handles:
- * - Fetching all workflows and folders from workspace
- * - Fetching workflow states and variables
- * - Creating ZIP file with all workspace data
- * - Downloading the ZIP file
- * - Loading state management
- * - Error handling and logging
- *
- * @param props - Hook configuration
- * @returns Export workspace handlers and state
*/
export function useExportWorkspace({ onSuccess }: UseExportWorkspaceProps = {}) {
const [isExporting, setIsExporting] = useState(false)
- /**
- * Export workspace to ZIP file
- */
const handleExportWorkspace = useCallback(
async (workspaceId: string, workspaceName: string) => {
if (isExporting) return
@@ -59,39 +47,15 @@ export function useExportWorkspace({ onSuccess }: UseExportWorkspaceProps = {})
const workflowsToExport: WorkflowExportData[] = []
for (const workflow of workflows) {
- try {
- const workflowResponse = await fetch(`/api/workflows/${workflow.id}`)
- if (!workflowResponse.ok) {
- logger.warn(`Failed to fetch workflow ${workflow.id}`)
- continue
- }
-
- const { data: workflowData } = await workflowResponse.json()
- if (!workflowData?.state) {
- logger.warn(`Workflow ${workflow.id} has no state`)
- continue
- }
-
- const variablesResponse = await fetch(`/api/workflows/${workflow.id}/variables`)
- let workflowVariables: Record | undefined
- if (variablesResponse.ok) {
- const variablesData = await variablesResponse.json()
- workflowVariables = variablesData?.data
- }
-
- workflowsToExport.push({
- workflow: {
- id: workflow.id,
- name: workflow.name,
- description: workflow.description,
- color: workflow.color,
- folderId: workflow.folderId,
- },
- state: workflowData.state,
- variables: workflowVariables,
- })
- } catch (error) {
- logger.error(`Failed to export workflow ${workflow.id}:`, error)
+ const exportData = await fetchWorkflowForExport(workflow.id, {
+ name: workflow.name,
+ description: workflow.description,
+ color: workflow.color,
+ folderId: workflow.folderId,
+ })
+
+ if (exportData) {
+ workflowsToExport.push(exportData)
}
}
@@ -109,14 +73,8 @@ export function useExportWorkspace({ onSuccess }: UseExportWorkspaceProps = {})
foldersToExport
)
- const blobUrl = URL.createObjectURL(zipBlob)
- const a = document.createElement('a')
- a.href = blobUrl
- a.download = `${workspaceName.replace(/[^a-z0-9]/gi, '-')}-${Date.now()}.zip`
- document.body.appendChild(a)
- a.click()
- document.body.removeChild(a)
- URL.revokeObjectURL(blobUrl)
+ const zipFilename = `${sanitizePathSegment(workspaceName)}-${Date.now()}.zip`
+ downloadFile(zipBlob, zipFilename, 'application/zip')
logger.info('Workspace exported successfully', {
workspaceId,
diff --git a/apps/sim/background/a2a-push-notification-delivery.ts b/apps/sim/background/a2a-push-notification-delivery.ts
new file mode 100644
index 0000000000..52c29aeb9a
--- /dev/null
+++ b/apps/sim/background/a2a-push-notification-delivery.ts
@@ -0,0 +1,33 @@
+import type { TaskState } from '@a2a-js/sdk'
+import { createLogger } from '@sim/logger'
+import { task } from '@trigger.dev/sdk'
+import { deliverPushNotification } from '@/lib/a2a/push-notifications'
+
+const logger = createLogger('A2APushNotificationDelivery')
+
+export interface A2APushNotificationParams {
+ taskId: string
+ state: TaskState
+}
+
+export const a2aPushNotificationTask = task({
+ id: 'a2a-push-notification-delivery',
+ retry: {
+ maxAttempts: 5,
+ minTimeoutInMs: 1000,
+ maxTimeoutInMs: 60000,
+ factor: 2,
+ },
+ run: async (params: A2APushNotificationParams) => {
+ logger.info('Delivering A2A push notification', params)
+
+ const success = await deliverPushNotification(params.taskId, params.state)
+
+ if (!success) {
+ throw new Error(`Failed to deliver push notification for task ${params.taskId}`)
+ }
+
+ logger.info('A2A push notification delivered successfully', params)
+ return { success: true, taskId: params.taskId, state: params.state }
+ },
+})
diff --git a/apps/sim/background/workflow-execution.ts b/apps/sim/background/workflow-execution.ts
index 9bb1686d66..bbe8a29e58 100644
--- a/apps/sim/background/workflow-execution.ts
+++ b/apps/sim/background/workflow-execution.ts
@@ -10,6 +10,7 @@ import { getWorkflowById } from '@/lib/workflows/utils'
import { ExecutionSnapshot } from '@/executor/execution/snapshot'
import type { ExecutionMetadata } from '@/executor/execution/types'
import type { ExecutionResult } from '@/executor/types'
+import type { CoreTriggerType } from '@/stores/logs/filters/types'
const logger = createLogger('TriggerWorkflowExecution')
@@ -17,7 +18,7 @@ export type WorkflowExecutionPayload = {
workflowId: string
userId: string
input?: any
- triggerType?: 'api' | 'webhook' | 'schedule' | 'manual' | 'chat' | 'mcp'
+ triggerType?: CoreTriggerType
metadata?: Record
}
diff --git a/apps/sim/blocks/blocks/a2a.ts b/apps/sim/blocks/blocks/a2a.ts
new file mode 100644
index 0000000000..a520028cff
--- /dev/null
+++ b/apps/sim/blocks/blocks/a2a.ts
@@ -0,0 +1,306 @@
+import { A2AIcon } from '@/components/icons'
+import type { BlockConfig } from '@/blocks/types'
+import type { ToolResponse } from '@/tools/types'
+
+export interface A2AResponse extends ToolResponse {
+ output: {
+ /** Response content from the agent */
+ content?: string
+ /** Task ID */
+ taskId?: string
+ /** Context ID for conversation continuity */
+ contextId?: string
+ /** Task state */
+ state?: string
+ /** Structured output artifacts */
+ artifacts?: Array<{
+ name?: string
+ description?: string
+ parts: Array<{ kind: string; text?: string; data?: unknown }>
+ }>
+ /** Full message history */
+ history?: Array<{
+ role: 'user' | 'agent'
+ parts: Array<{ kind: string; text?: string }>
+ }>
+ /** Whether cancellation was successful (cancel_task) */
+ cancelled?: boolean
+ /** Whether task is still running (resubscribe) */
+ isRunning?: boolean
+ /** Agent name (get_agent_card) */
+ name?: string
+ /** Agent description (get_agent_card) */
+ description?: string
+ /** Agent URL (get_agent_card) */
+ url?: string
+ /** Agent version (get_agent_card) */
+ version?: string
+ /** Agent capabilities (get_agent_card) */
+ capabilities?: Record
+ /** Agent skills (get_agent_card) */
+ skills?: Array<{ id: string; name: string; description?: string }>
+ /** Agent authentication schemes (get_agent_card) */
+ authentication?: { schemes: string[] }
+ /** Push notification webhook URL */
+ webhookUrl?: string
+ /** Push notification token */
+ token?: string
+ /** Whether push notification config exists */
+ exists?: boolean
+ /** Operation success indicator */
+ success?: boolean
+ }
+}
+
+export const A2ABlock: BlockConfig = {
+ type: 'a2a',
+ name: 'A2A',
+ description: 'Interact with external A2A-compatible agents',
+ longDescription:
+ 'Use the A2A (Agent-to-Agent) protocol to interact with external AI agents. ' +
+ 'Send messages, query task status, cancel tasks, or discover agent capabilities. ' +
+ 'Compatible with any A2A-compliant agent including LangGraph, Google ADK, and other Sim workflows.',
+ docsLink: 'https://docs.sim.ai/blocks/a2a',
+ category: 'tools',
+ bgColor: '#4151B5',
+ icon: A2AIcon,
+ subBlocks: [
+ {
+ id: 'operation',
+ title: 'Operation',
+ type: 'dropdown',
+ options: [
+ { label: 'Send Message', id: 'a2a_send_message' },
+ { label: 'Get Task', id: 'a2a_get_task' },
+ { label: 'Cancel Task', id: 'a2a_cancel_task' },
+ { label: 'Get Agent Card', id: 'a2a_get_agent_card' },
+ { label: 'Resubscribe', id: 'a2a_resubscribe' },
+ { label: 'Set Push Notification', id: 'a2a_set_push_notification' },
+ { label: 'Get Push Notification', id: 'a2a_get_push_notification' },
+ { label: 'Delete Push Notification', id: 'a2a_delete_push_notification' },
+ ],
+ defaultValue: 'a2a_send_message',
+ },
+ {
+ id: 'agentUrl',
+ title: 'Agent URL',
+ type: 'short-input',
+ placeholder: 'https://api.example.com/a2a/serve/agent-id',
+ required: true,
+ description: 'The A2A endpoint URL',
+ },
+ {
+ id: 'message',
+ title: 'Message',
+ type: 'long-input',
+ placeholder: 'Enter your message to the agent...',
+ description: 'The message to send to the agent',
+ condition: { field: 'operation', value: 'a2a_send_message' },
+ required: true,
+ },
+ {
+ id: 'taskId',
+ title: 'Task ID',
+ type: 'short-input',
+ placeholder: 'Task ID',
+ description: 'Task ID to query, cancel, continue, or configure',
+ condition: {
+ field: 'operation',
+ value: [
+ 'a2a_send_message',
+ 'a2a_get_task',
+ 'a2a_cancel_task',
+ 'a2a_resubscribe',
+ 'a2a_set_push_notification',
+ 'a2a_get_push_notification',
+ 'a2a_delete_push_notification',
+ ],
+ },
+ required: {
+ field: 'operation',
+ value: [
+ 'a2a_get_task',
+ 'a2a_cancel_task',
+ 'a2a_resubscribe',
+ 'a2a_set_push_notification',
+ 'a2a_get_push_notification',
+ 'a2a_delete_push_notification',
+ ],
+ },
+ },
+ {
+ id: 'contextId',
+ title: 'Context ID',
+ type: 'short-input',
+ placeholder: 'Optional - for multi-turn conversations',
+ description: 'Context ID for conversation continuity across tasks',
+ condition: { field: 'operation', value: 'a2a_send_message' },
+ },
+ {
+ id: 'historyLength',
+ title: 'History Length',
+ type: 'short-input',
+ placeholder: 'Number of messages to include',
+ description: 'Number of history messages to include in the response',
+ condition: { field: 'operation', value: 'a2a_get_task' },
+ },
+ {
+ id: 'webhookUrl',
+ title: 'Webhook URL',
+ type: 'short-input',
+ placeholder: 'https://your-app.com/webhook',
+ description: 'HTTPS webhook URL to receive task update notifications',
+ condition: { field: 'operation', value: 'a2a_set_push_notification' },
+ required: { field: 'operation', value: 'a2a_set_push_notification' },
+ },
+ {
+ id: 'token',
+ title: 'Webhook Token',
+ type: 'short-input',
+ password: true,
+ placeholder: 'Optional token for webhook validation',
+ description: 'Token that will be included in webhook requests for validation',
+ condition: { field: 'operation', value: 'a2a_set_push_notification' },
+ },
+ {
+ id: 'apiKey',
+ title: 'API Key',
+ type: 'short-input',
+ password: true,
+ placeholder: 'Optional API key for authenticated agents',
+ description:
+ 'Optional API key sent via X-API-Key header for agents that require authentication',
+ },
+ ],
+ tools: {
+ access: [
+ 'a2a_send_message',
+ 'a2a_get_task',
+ 'a2a_cancel_task',
+ 'a2a_get_agent_card',
+ 'a2a_resubscribe',
+ 'a2a_set_push_notification',
+ 'a2a_get_push_notification',
+ 'a2a_delete_push_notification',
+ ],
+ config: {
+ tool: (params) => params.operation as string,
+ },
+ },
+ inputs: {
+ operation: {
+ type: 'string',
+ description: 'A2A operation to perform',
+ },
+ agentUrl: {
+ type: 'string',
+ description: 'A2A endpoint URL',
+ },
+ message: {
+ type: 'string',
+ description: 'Message to send to the agent',
+ },
+ taskId: {
+ type: 'string',
+ description: 'Task ID to query, cancel, continue, or configure',
+ },
+ contextId: {
+ type: 'string',
+ description: 'Context ID for conversation continuity',
+ },
+ historyLength: {
+ type: 'number',
+ description: 'Number of history messages to include',
+ },
+ webhookUrl: {
+ type: 'string',
+ description: 'HTTPS webhook URL for push notifications',
+ },
+ token: {
+ type: 'string',
+ description: 'Token for webhook validation',
+ },
+ apiKey: {
+ type: 'string',
+ description: 'API key for authentication',
+ },
+ },
+ outputs: {
+ content: {
+ type: 'string',
+ description: 'The text response from the agent',
+ },
+ taskId: {
+ type: 'string',
+ description: 'Task ID for follow-up interactions',
+ },
+ contextId: {
+ type: 'string',
+ description: 'Context ID for conversation continuity',
+ },
+ state: {
+ type: 'string',
+ description: 'Task state (completed, failed, etc.)',
+ },
+ artifacts: {
+ type: 'array',
+ description: 'Structured output artifacts from the agent',
+ },
+ history: {
+ type: 'array',
+ description: 'Full message history of the conversation',
+ },
+ cancelled: {
+ type: 'boolean',
+ description: 'Whether the task was successfully cancelled',
+ },
+ isRunning: {
+ type: 'boolean',
+ description: 'Whether the task is still running',
+ },
+ name: {
+ type: 'string',
+ description: 'Agent name',
+ },
+ description: {
+ type: 'string',
+ description: 'Agent description',
+ },
+ url: {
+ type: 'string',
+ description: 'Agent endpoint URL',
+ },
+ version: {
+ type: 'string',
+ description: 'Agent version',
+ },
+ capabilities: {
+ type: 'json',
+ description: 'Agent capabilities (streaming, pushNotifications, etc.)',
+ },
+ skills: {
+ type: 'array',
+ description: 'Skills the agent can perform',
+ },
+ authentication: {
+ type: 'json',
+ description: 'Supported authentication schemes',
+ },
+ webhookUrl: {
+ type: 'string',
+ description: 'Configured webhook URL',
+ },
+ token: {
+ type: 'string',
+ description: 'Webhook validation token',
+ },
+ exists: {
+ type: 'boolean',
+ description: 'Whether push notification config exists',
+ },
+ success: {
+ type: 'boolean',
+ description: 'Whether the operation was successful',
+ },
+ },
+}
diff --git a/apps/sim/blocks/registry.ts b/apps/sim/blocks/registry.ts
index 85a5b7ac2f..06752d5d68 100644
--- a/apps/sim/blocks/registry.ts
+++ b/apps/sim/blocks/registry.ts
@@ -1,3 +1,4 @@
+import { A2ABlock } from '@/blocks/blocks/a2a'
import { AgentBlock } from '@/blocks/blocks/agent'
import { AhrefsBlock } from '@/blocks/blocks/ahrefs'
import { AirtableBlock } from '@/blocks/blocks/airtable'
@@ -148,6 +149,7 @@ import { SQSBlock } from './blocks/sqs'
// Registry of all available blocks, alphabetically sorted
export const registry: Record = {
+ a2a: A2ABlock,
agent: AgentBlock,
ahrefs: AhrefsBlock,
airtable: AirtableBlock,
diff --git a/apps/sim/components/emcn/components/badge/badge.tsx b/apps/sim/components/emcn/components/badge/badge.tsx
index 4e363178ed..7b728df058 100644
--- a/apps/sim/components/emcn/components/badge/badge.tsx
+++ b/apps/sim/components/emcn/components/badge/badge.tsx
@@ -25,6 +25,7 @@ const badgeVariants = cva(
orange: `${STATUS_BASE} bg-[#fed7aa] text-[#c2410c] dark:bg-[rgba(249,115,22,0.2)] dark:text-[#fdba74]`,
amber: `${STATUS_BASE} bg-[#fde68a] text-[#a16207] dark:bg-[rgba(245,158,11,0.2)] dark:text-[#fcd34d]`,
teal: `${STATUS_BASE} bg-[#99f6e4] text-[#0f766e] dark:bg-[rgba(20,184,166,0.2)] dark:text-[#5eead4]`,
+ cyan: `${STATUS_BASE} bg-[#a5f3fc] text-[#0e7490] dark:bg-[rgba(14,165,233,0.2)] dark:text-[#7dd3fc]`,
'gray-secondary': `${STATUS_BASE} bg-[var(--surface-4)] text-[var(--text-secondary)]`,
},
size: {
@@ -51,6 +52,7 @@ const STATUS_VARIANTS = [
'orange',
'amber',
'teal',
+ 'cyan',
'gray-secondary',
] as const
@@ -84,7 +86,7 @@ export interface BadgeProps
* Supports two categories of variants:
* - **Bordered**: `default`, `outline` - traditional badges with borders
* - **Status colors**: `green`, `red`, `gray`, `blue`, `blue-secondary`, `purple`,
- * `orange`, `amber`, `teal`, `gray-secondary` - borderless colored badges
+ * `orange`, `amber`, `teal`, `cyan`, `gray-secondary` - borderless colored badges
*
* Status color variants can display a dot indicator via the `dot` prop.
* All variants support an optional `icon` prop for leading icons.
diff --git a/apps/sim/components/emcn/components/combobox/combobox.tsx b/apps/sim/components/emcn/components/combobox/combobox.tsx
index 19dc56a343..4cb4748afa 100644
--- a/apps/sim/components/emcn/components/combobox/combobox.tsx
+++ b/apps/sim/components/emcn/components/combobox/combobox.tsx
@@ -59,6 +59,8 @@ export type ComboboxOption = {
export type ComboboxOptionGroup = {
/** Optional section header label */
section?: string
+ /** Optional custom section header element (overrides section label) */
+ sectionElement?: ReactNode
/** Options in this group */
items: ComboboxOption[]
}
@@ -625,11 +627,13 @@ const Combobox = forwardRef(
{filteredGroups.map((group, groupIndex) => (
- {group.section && (
-
- {group.section}
-
- )}
+ {group.sectionElement
+ ? group.sectionElement
+ : group.section && (
+
+ {group.section}
+
+ )}
{group.items.map((option) => {
const isSelected = multiSelect
? multiSelectValues?.includes(option.value)
diff --git a/apps/sim/components/emcn/components/tag-input/tag-input.tsx b/apps/sim/components/emcn/components/tag-input/tag-input.tsx
index 8a1c2e832a..18a661d00b 100644
--- a/apps/sim/components/emcn/components/tag-input/tag-input.tsx
+++ b/apps/sim/components/emcn/components/tag-input/tag-input.tsx
@@ -53,6 +53,8 @@ const tagVariants = cva(
variants: {
variant: {
default: 'bg-[#bfdbfe] text-[#1d4ed8] dark:bg-[rgba(59,130,246,0.2)] dark:text-[#93c5fd]',
+ secondary:
+ 'border border-[var(--border-1)] bg-[var(--surface-4)] text-[var(--text-secondary)] hover:text-[var(--text-primary)]',
invalid:
'bg-[#fecaca] text-[var(--text-error)] dark:bg-[#551a1a] dark:text-[var(--text-error)]',
},
@@ -102,7 +104,9 @@ const Tag = React.memo(function Tag({
'flex-shrink-0 opacity-80 transition-opacity hover:opacity-100 focus:outline-none',
variant === 'invalid'
? 'text-[var(--text-error)]'
- : 'text-[#1d4ed8] dark:text-[#93c5fd]'
+ : variant === 'secondary'
+ ? 'text-[var(--text-tertiary)]'
+ : 'text-[#1d4ed8] dark:text-[#93c5fd]'
)}
aria-label={`Remove ${value}`}
>
@@ -192,6 +196,8 @@ export interface TagInputProps extends VariantProps
{
renderTagSuffix?: (value: string, index: number) => React.ReactNode
/** Options for enabling file input (drag/drop and file picker) */
fileInputOptions?: FileInputOptions
+ /** Variant for valid tags (defaults to 'default') */
+ tagVariant?: 'default' | 'secondary'
}
/**
@@ -222,6 +228,7 @@ const TagInput = React.forwardRef(
triggerKeys = ['Enter', ',', ' '],
renderTagSuffix,
fileInputOptions,
+ tagVariant = 'default',
variant,
},
ref
@@ -399,7 +406,7 @@ const TagInput = React.forwardRef(
onRemove(item.value, index, item.isValid)}
disabled={disabled}
suffix={item.isValid ? renderTagSuffix?.(item.value, index) : undefined}
@@ -409,7 +416,7 @@ const TagInput = React.forwardRef(
className={cn(
'flex items-center',
inputValue.trim() &&
- cn(tagVariants({ variant: 'default' }), 'gap-0 py-0 pr-0 pl-[4px] opacity-80')
+ cn(tagVariants({ variant: tagVariant }), 'gap-0 py-0 pr-0 pl-[4px] opacity-80')
)}
>
diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx
index 91803c3316..0143e517a5 100644
--- a/apps/sim/components/icons.tsx
+++ b/apps/sim/components/icons.tsx
@@ -4086,6 +4086,31 @@ export function McpIcon(props: SVGProps
) {
)
}
+export function A2AIcon(props: SVGProps) {
+ return (
+
+
+
+
+
+
+
+
+
+ )
+}
+
export function WordpressIcon(props: SVGProps) {
return (
diff --git a/apps/sim/hooks/queries/a2a/agents.ts b/apps/sim/hooks/queries/a2a/agents.ts
new file mode 100644
index 0000000000..4a277ebac1
--- /dev/null
+++ b/apps/sim/hooks/queries/a2a/agents.ts
@@ -0,0 +1,307 @@
+/**
+ * A2A Agents React Query Hooks
+ *
+ * Hooks for managing A2A agents in the UI.
+ */
+
+import type { AgentCapabilities, AgentSkill } from '@a2a-js/sdk'
+import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
+import type { AgentAuthentication } from '@/lib/a2a/types'
+
+/**
+ * A2A Agent as returned from the API
+ */
+export interface A2AAgent {
+ id: string
+ workspaceId: string
+ workflowId: string
+ name: string
+ description?: string
+ version: string
+ capabilities: AgentCapabilities
+ skills: AgentSkill[]
+ authentication: AgentAuthentication
+ isPublished: boolean
+ publishedAt?: string
+ createdAt: string
+ updatedAt: string
+ workflowName?: string
+ workflowDescription?: string
+ isDeployed?: boolean
+ taskCount?: number
+}
+
+/**
+ * Query keys for A2A agents
+ */
+export const a2aAgentKeys = {
+ all: ['a2a-agents'] as const,
+ list: (workspaceId: string) => [...a2aAgentKeys.all, 'list', workspaceId] as const,
+ detail: (agentId: string) => [...a2aAgentKeys.all, 'detail', agentId] as const,
+}
+
+/**
+ * Fetch A2A agents for a workspace
+ */
+async function fetchA2AAgents(workspaceId: string): Promise {
+ const response = await fetch(`/api/a2a/agents?workspaceId=${workspaceId}`)
+ if (!response.ok) {
+ throw new Error('Failed to fetch A2A agents')
+ }
+ const data = await response.json()
+ return data.agents
+}
+
+/**
+ * Hook to list A2A agents for a workspace
+ */
+export function useA2AAgents(workspaceId: string) {
+ return useQuery({
+ queryKey: a2aAgentKeys.list(workspaceId),
+ queryFn: () => fetchA2AAgents(workspaceId),
+ enabled: Boolean(workspaceId),
+ staleTime: 60 * 1000, // 1 minute
+ })
+}
+
+/**
+ * Agent Card as returned from the agent detail endpoint
+ */
+export interface A2AAgentCard {
+ name: string
+ description?: string
+ url: string
+ version: string
+ documentationUrl?: string
+ provider?: {
+ organization: string
+ url?: string
+ }
+ capabilities: AgentCapabilities
+ skills: AgentSkill[]
+ authentication?: AgentAuthentication
+ defaultInputModes?: string[]
+ defaultOutputModes?: string[]
+}
+
+/**
+ * Fetch a single A2A agent card (discovery document)
+ */
+async function fetchA2AAgentCard(agentId: string): Promise {
+ const response = await fetch(`/api/a2a/agents/${agentId}`)
+ if (!response.ok) {
+ throw new Error('Failed to fetch A2A agent')
+ }
+ return response.json()
+}
+
+/**
+ * Hook to get a single A2A agent card (discovery document)
+ */
+export function useA2AAgentCard(agentId: string) {
+ return useQuery({
+ queryKey: a2aAgentKeys.detail(agentId),
+ queryFn: () => fetchA2AAgentCard(agentId),
+ enabled: Boolean(agentId),
+ })
+}
+
+/**
+ * Create A2A agent params
+ */
+export interface CreateA2AAgentParams {
+ workspaceId: string
+ workflowId: string
+ name?: string
+ description?: string
+ capabilities?: AgentCapabilities
+ authentication?: AgentAuthentication
+ skillTags?: string[]
+}
+
+/**
+ * Create a new A2A agent
+ */
+async function createA2AAgent(params: CreateA2AAgentParams): Promise {
+ const response = await fetch('/api/a2a/agents', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(params),
+ })
+ if (!response.ok) {
+ const error = await response.json()
+ throw new Error(error.error || 'Failed to create A2A agent')
+ }
+ const data = await response.json()
+ return data.agent
+}
+
+/**
+ * Hook to create an A2A agent
+ */
+export function useCreateA2AAgent() {
+ const queryClient = useQueryClient()
+
+ return useMutation({
+ mutationFn: createA2AAgent,
+ onSuccess: () => {
+ // Invalidate all a2a-agent queries (list, detail, byWorkflow, etc.)
+ queryClient.invalidateQueries({
+ queryKey: a2aAgentKeys.all,
+ })
+ },
+ })
+}
+
+/**
+ * Update A2A agent params
+ */
+export interface UpdateA2AAgentParams {
+ agentId: string
+ name?: string
+ description?: string
+ version?: string
+ capabilities?: AgentCapabilities
+ skills?: AgentSkill[]
+ authentication?: AgentAuthentication
+ isPublished?: boolean
+ skillTags?: string[]
+}
+
+/**
+ * Update an A2A agent
+ */
+async function updateA2AAgent(params: UpdateA2AAgentParams): Promise {
+ const { agentId, ...body } = params
+ const response = await fetch(`/api/a2a/agents/${agentId}`, {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(body),
+ })
+ if (!response.ok) {
+ const error = await response.json()
+ throw new Error(error.error || 'Failed to update A2A agent')
+ }
+ const data = await response.json()
+ return data.agent
+}
+
+/**
+ * Hook to update an A2A agent
+ */
+export function useUpdateA2AAgent() {
+ const queryClient = useQueryClient()
+
+ return useMutation({
+ mutationFn: updateA2AAgent,
+ onSuccess: () => {
+ // Invalidate all a2a-agent queries (list, detail, byWorkflow, etc.)
+ queryClient.invalidateQueries({
+ queryKey: a2aAgentKeys.all,
+ })
+ },
+ })
+}
+
+/**
+ * Delete an A2A agent
+ */
+async function deleteA2AAgent(params: { agentId: string; workspaceId: string }): Promise {
+ const response = await fetch(`/api/a2a/agents/${params.agentId}`, {
+ method: 'DELETE',
+ })
+ if (!response.ok) {
+ const error = await response.json()
+ throw new Error(error.error || 'Failed to delete A2A agent')
+ }
+}
+
+/**
+ * Hook to delete an A2A agent
+ */
+export function useDeleteA2AAgent() {
+ const queryClient = useQueryClient()
+
+ return useMutation({
+ mutationFn: deleteA2AAgent,
+ onSuccess: () => {
+ // Invalidate all a2a-agent queries (list, detail, byWorkflow, etc.)
+ queryClient.invalidateQueries({
+ queryKey: a2aAgentKeys.all,
+ })
+ },
+ })
+}
+
+/**
+ * Publish/unpublish agent params
+ */
+export interface PublishA2AAgentParams {
+ agentId: string
+ workspaceId: string
+ action: 'publish' | 'unpublish' | 'refresh'
+}
+
+/**
+ * Publish or unpublish an A2A agent
+ */
+async function publishA2AAgent(params: PublishA2AAgentParams): Promise<{
+ isPublished?: boolean
+ skills?: AgentSkill[]
+}> {
+ const response = await fetch(`/api/a2a/agents/${params.agentId}`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ action: params.action }),
+ })
+ if (!response.ok) {
+ const error = await response.json()
+ throw new Error(error.error || 'Failed to update A2A agent')
+ }
+ return response.json()
+}
+
+/**
+ * Hook to publish/unpublish an A2A agent
+ */
+export function usePublishA2AAgent() {
+ const queryClient = useQueryClient()
+
+ return useMutation({
+ mutationFn: publishA2AAgent,
+ onSuccess: () => {
+ // Invalidate all a2a-agent queries (list, detail, byWorkflow, etc.)
+ queryClient.invalidateQueries({
+ queryKey: a2aAgentKeys.all,
+ })
+ },
+ })
+}
+
+/**
+ * Fetch A2A agent by workflow ID
+ */
+async function fetchA2AAgentByWorkflow(
+ workspaceId: string,
+ workflowId: string
+): Promise {
+ const response = await fetch(`/api/a2a/agents?workspaceId=${workspaceId}`)
+ if (!response.ok) {
+ throw new Error('Failed to fetch A2A agents')
+ }
+ const data = await response.json()
+ const agents = data.agents as A2AAgent[]
+ return agents.find((agent) => agent.workflowId === workflowId) || null
+}
+
+/**
+ * Hook to get A2A agent by workflow ID
+ */
+export function useA2AAgentByWorkflow(workspaceId: string, workflowId: string) {
+ return useQuery({
+ queryKey: [...a2aAgentKeys.all, 'byWorkflow', workspaceId, workflowId] as const,
+ queryFn: () => fetchA2AAgentByWorkflow(workspaceId, workflowId),
+ enabled: Boolean(workspaceId) && Boolean(workflowId),
+ staleTime: 30 * 1000, // 30 seconds
+ })
+}
diff --git a/apps/sim/hooks/queries/a2a/tasks.ts b/apps/sim/hooks/queries/a2a/tasks.ts
new file mode 100644
index 0000000000..1e4cd83163
--- /dev/null
+++ b/apps/sim/hooks/queries/a2a/tasks.ts
@@ -0,0 +1,262 @@
+/**
+ * A2A Tasks React Query Hooks (v0.3)
+ *
+ * Hooks for interacting with A2A tasks in the UI.
+ */
+
+import type { Artifact, Message, TaskState } from '@a2a-js/sdk'
+import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
+import { isTerminalState } from '@/lib/a2a/utils'
+
+/** A2A v0.3 JSON-RPC method names */
+const A2A_METHODS = {
+ MESSAGE_SEND: 'message/send',
+ TASKS_GET: 'tasks/get',
+ TASKS_CANCEL: 'tasks/cancel',
+} as const
+
+/**
+ * A2A Task as returned from queries
+ */
+export interface A2ATask {
+ kind: 'task'
+ id: string
+ contextId?: string
+ status: {
+ state: TaskState
+ timestamp?: string
+ message?: string
+ }
+ history?: Message[]
+ artifacts?: Artifact[]
+ metadata?: Record
+}
+
+/**
+ * Query keys for A2A tasks
+ */
+export const a2aTaskKeys = {
+ all: ['a2a-tasks'] as const,
+ detail: (agentUrl: string, taskId: string) =>
+ [...a2aTaskKeys.all, 'detail', agentUrl, taskId] as const,
+}
+
+/**
+ * Send task params
+ */
+export interface SendA2ATaskParams {
+ agentUrl: string
+ message: string
+ taskId?: string
+ contextId?: string
+ apiKey?: string
+}
+
+/**
+ * Send task response
+ */
+export interface SendA2ATaskResponse {
+ content: string
+ taskId: string
+ contextId?: string
+ state: TaskState
+ artifacts?: Artifact[]
+ history?: Message[]
+}
+
+/**
+ * Send a message to an A2A agent (v0.3)
+ */
+async function sendA2ATask(params: SendA2ATaskParams): Promise {
+ const userMessage: Message = {
+ kind: 'message',
+ messageId: crypto.randomUUID(),
+ role: 'user',
+ parts: [{ kind: 'text', text: params.message }],
+ ...(params.taskId && { taskId: params.taskId }),
+ ...(params.contextId && { contextId: params.contextId }),
+ }
+
+ const response = await fetch(params.agentUrl, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(params.apiKey ? { Authorization: `Bearer ${params.apiKey}` } : {}),
+ },
+ body: JSON.stringify({
+ jsonrpc: '2.0',
+ id: crypto.randomUUID(),
+ method: A2A_METHODS.MESSAGE_SEND,
+ params: {
+ message: userMessage,
+ },
+ }),
+ })
+
+ if (!response.ok) {
+ throw new Error(`A2A request failed: ${response.status} ${response.statusText}`)
+ }
+
+ const result = await response.json()
+
+ if (result.error) {
+ throw new Error(result.error.message || 'A2A request failed')
+ }
+
+ const task = result.result as A2ATask
+
+ const lastAgentMessage = task.history?.filter((m) => m.role === 'agent').pop()
+ const content = lastAgentMessage
+ ? lastAgentMessage.parts
+ .filter((p): p is import('@a2a-js/sdk').TextPart => p.kind === 'text')
+ .map((p) => p.text)
+ .join('')
+ : ''
+
+ return {
+ content,
+ taskId: task.id,
+ contextId: task.contextId,
+ state: task.status.state,
+ artifacts: task.artifacts,
+ history: task.history,
+ }
+}
+
+/**
+ * Hook to send a message to an A2A agent
+ */
+export function useSendA2ATask() {
+ const queryClient = useQueryClient()
+
+ return useMutation({
+ mutationFn: sendA2ATask,
+ onSuccess: (data, variables) => {
+ queryClient.invalidateQueries({
+ queryKey: a2aTaskKeys.detail(variables.agentUrl, data.taskId),
+ })
+ },
+ })
+}
+
+/**
+ * Get task params
+ */
+export interface GetA2ATaskParams {
+ agentUrl: string
+ taskId: string
+ apiKey?: string
+ historyLength?: number
+}
+
+/**
+ * Fetch a task from an A2A agent
+ */
+async function fetchA2ATask(params: GetA2ATaskParams): Promise {
+ const response = await fetch(params.agentUrl, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(params.apiKey ? { Authorization: `Bearer ${params.apiKey}` } : {}),
+ },
+ body: JSON.stringify({
+ jsonrpc: '2.0',
+ id: crypto.randomUUID(),
+ method: A2A_METHODS.TASKS_GET,
+ params: {
+ id: params.taskId,
+ historyLength: params.historyLength,
+ },
+ }),
+ })
+
+ if (!response.ok) {
+ throw new Error(`A2A request failed: ${response.status} ${response.statusText}`)
+ }
+
+ const result = await response.json()
+
+ if (result.error) {
+ throw new Error(result.error.message || 'A2A request failed')
+ }
+
+ return result.result as A2ATask
+}
+
+/**
+ * Hook to get an A2A task
+ */
+export function useA2ATask(params: GetA2ATaskParams | null) {
+ return useQuery({
+ queryKey: params ? a2aTaskKeys.detail(params.agentUrl, params.taskId) : ['disabled'],
+ queryFn: () => fetchA2ATask(params!),
+ enabled: Boolean(params?.agentUrl && params?.taskId),
+ staleTime: 5 * 1000, // 5 seconds - tasks can change quickly
+ refetchInterval: (query) => {
+ // Auto-refresh if task is still running
+ const data = query.state.data as A2ATask | undefined
+ if (data && !isTerminalState(data.status.state)) {
+ return 2000 // 2 seconds
+ }
+ return false
+ },
+ })
+}
+
+/**
+ * Cancel task params
+ */
+export interface CancelA2ATaskParams {
+ agentUrl: string
+ taskId: string
+ apiKey?: string
+}
+
+/**
+ * Cancel a task
+ */
+async function cancelA2ATask(params: CancelA2ATaskParams): Promise {
+ const response = await fetch(params.agentUrl, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(params.apiKey ? { Authorization: `Bearer ${params.apiKey}` } : {}),
+ },
+ body: JSON.stringify({
+ jsonrpc: '2.0',
+ id: crypto.randomUUID(),
+ method: A2A_METHODS.TASKS_CANCEL,
+ params: {
+ id: params.taskId,
+ },
+ }),
+ })
+
+ if (!response.ok) {
+ throw new Error(`A2A request failed: ${response.status} ${response.statusText}`)
+ }
+
+ const result = await response.json()
+
+ if (result.error) {
+ throw new Error(result.error.message || 'A2A request failed')
+ }
+
+ return result.result as A2ATask
+}
+
+/**
+ * Hook to cancel an A2A task
+ */
+export function useCancelA2ATask() {
+ const queryClient = useQueryClient()
+
+ return useMutation({
+ mutationFn: cancelA2ATask,
+ onSuccess: (data, variables) => {
+ queryClient.invalidateQueries({
+ queryKey: a2aTaskKeys.detail(variables.agentUrl, variables.taskId),
+ })
+ },
+ })
+}
diff --git a/apps/sim/hooks/queries/notifications.ts b/apps/sim/hooks/queries/notifications.ts
index ebe8b5b1f4..49af2ed8d5 100644
--- a/apps/sim/hooks/queries/notifications.ts
+++ b/apps/sim/hooks/queries/notifications.ts
@@ -1,5 +1,6 @@
import { createLogger } from '@sim/logger'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
+import type { CoreTriggerType } from '@/stores/logs/filters/types'
const logger = createLogger('NotificationQueries')
@@ -18,7 +19,7 @@ export const notificationKeys = {
type NotificationType = 'webhook' | 'email' | 'slack'
type LogLevel = 'info' | 'error'
-type TriggerType = 'api' | 'webhook' | 'schedule' | 'manual' | 'chat' | 'mcp'
+type TriggerType = CoreTriggerType
type AlertRuleType =
| 'consecutive_failures'
diff --git a/apps/sim/lib/a2a/agent-card.ts b/apps/sim/lib/a2a/agent-card.ts
new file mode 100644
index 0000000000..b80fd6476f
--- /dev/null
+++ b/apps/sim/lib/a2a/agent-card.ts
@@ -0,0 +1,138 @@
+import { getBaseUrl } from '@/lib/core/utils/urls'
+import {
+ A2A_DEFAULT_CAPABILITIES,
+ A2A_DEFAULT_INPUT_MODES,
+ A2A_DEFAULT_OUTPUT_MODES,
+ A2A_PROTOCOL_VERSION,
+} from './constants'
+import type { AgentCapabilities, AgentSkill } from './types'
+import { buildA2AEndpointUrl, sanitizeAgentName } from './utils'
+
+export interface AppAgentCard {
+ name: string
+ description: string
+ url: string
+ protocolVersion: string
+ documentationUrl?: string
+ provider?: {
+ organization: string
+ url: string
+ }
+ capabilities: AgentCapabilities
+ skills: AgentSkill[]
+ defaultInputModes: string[]
+ defaultOutputModes: string[]
+}
+
+interface WorkflowData {
+ id: string
+ name: string
+ description?: string | null
+}
+
+interface AgentData {
+ id: string
+ name: string
+ description?: string | null
+ version: string
+ capabilities?: AgentCapabilities
+ skills?: AgentSkill[]
+}
+
+export function generateAgentCard(agent: AgentData, workflow: WorkflowData): AppAgentCard {
+ const baseUrl = getBaseUrl()
+ const description =
+ agent.description || workflow.description || `${agent.name} - A2A Agent powered by Sim`
+
+ return {
+ name: agent.name,
+ description,
+ url: buildA2AEndpointUrl(baseUrl, agent.id),
+ protocolVersion: A2A_PROTOCOL_VERSION,
+ documentationUrl: `${baseUrl}/docs/a2a`,
+ provider: {
+ organization: 'Sim',
+ url: baseUrl,
+ },
+ capabilities: {
+ ...A2A_DEFAULT_CAPABILITIES,
+ ...agent.capabilities,
+ },
+ skills: agent.skills || [
+ {
+ id: 'execute',
+ name: `Execute ${workflow.name}`,
+ description: workflow.description || `Execute the ${workflow.name} workflow`,
+ tags: [],
+ },
+ ],
+ defaultInputModes: [...A2A_DEFAULT_INPUT_MODES],
+ defaultOutputModes: [...A2A_DEFAULT_OUTPUT_MODES],
+ }
+}
+
+export function generateSkillsFromWorkflow(
+ workflowName: string,
+ workflowDescription: string | undefined | null,
+ tags?: string[]
+): AgentSkill[] {
+ const skill: AgentSkill = {
+ id: 'execute',
+ name: `Execute ${workflowName}`,
+ description: workflowDescription || `Execute the ${workflowName} workflow`,
+ tags: tags || [],
+ }
+
+ return [skill]
+}
+
+export function generateDefaultAgentName(workflowName: string): string {
+ return sanitizeAgentName(workflowName)
+}
+
+export function validateAgentCard(card: unknown): card is AppAgentCard {
+ if (!card || typeof card !== 'object') return false
+
+ const c = card as Record
+
+ if (typeof c.name !== 'string' || !c.name) return false
+ if (typeof c.url !== 'string' || !c.url) return false
+ if (typeof c.description !== 'string') return false
+
+ // Validate URL format
+ try {
+ const url = new URL(c.url)
+ if (!['http:', 'https:'].includes(url.protocol)) return false
+ } catch {
+ return false
+ }
+
+ if (c.capabilities && typeof c.capabilities !== 'object') return false
+
+ if (!Array.isArray(c.skills)) return false
+
+ return true
+}
+
+export function mergeAgentCard(
+ existing: AppAgentCard,
+ updates: Partial
+): AppAgentCard {
+ return {
+ ...existing,
+ ...updates,
+ capabilities: {
+ ...existing.capabilities,
+ ...updates.capabilities,
+ },
+ skills: updates.skills || existing.skills,
+ }
+}
+
+export function getAgentCardPaths(agentId: string) {
+ const baseUrl = getBaseUrl()
+ return {
+ card: `${baseUrl}/api/a2a/agents/${agentId}`,
+ serve: `${baseUrl}/api/a2a/serve/${agentId}`,
+ }
+}
diff --git a/apps/sim/lib/a2a/constants.ts b/apps/sim/lib/a2a/constants.ts
new file mode 100644
index 0000000000..9027151857
--- /dev/null
+++ b/apps/sim/lib/a2a/constants.ts
@@ -0,0 +1,28 @@
+export { AGENT_CARD_PATH } from '@a2a-js/sdk'
+export const A2A_PROTOCOL_VERSION = '0.3.0'
+
+export const A2A_DEFAULT_TIMEOUT = 300000
+
+/**
+ * Maximum number of messages stored per task in the database.
+ * Messages beyond this limit should be truncated to prevent unbounded array growth.
+ * For capacity planning: ~100 messages * ~1KB avg = ~100KB per task max.
+ */
+export const A2A_MAX_HISTORY_LENGTH = 100
+
+export const A2A_DEFAULT_CAPABILITIES = {
+ streaming: true,
+ pushNotifications: false,
+ stateTransitionHistory: true,
+} as const
+
+export const A2A_DEFAULT_INPUT_MODES = ['text'] as const
+
+export const A2A_DEFAULT_OUTPUT_MODES = ['text'] as const
+
+export const A2A_CACHE = {
+ AGENT_CARD_TTL: 3600, // 1 hour
+ TASK_TTL: 86400, // 24 hours
+} as const
+
+export const A2A_TERMINAL_STATES = ['completed', 'failed', 'canceled', 'rejected'] as const
diff --git a/apps/sim/lib/a2a/index.ts b/apps/sim/lib/a2a/index.ts
new file mode 100644
index 0000000000..d9cc30c0fc
--- /dev/null
+++ b/apps/sim/lib/a2a/index.ts
@@ -0,0 +1,83 @@
+import type { AppAgentCard } from './agent-card'
+import {
+ generateAgentCard,
+ generateDefaultAgentName,
+ generateSkillsFromWorkflow,
+ getAgentCardPaths,
+ mergeAgentCard,
+ validateAgentCard,
+} from './agent-card'
+import {
+ A2A_CACHE,
+ A2A_DEFAULT_CAPABILITIES,
+ A2A_DEFAULT_INPUT_MODES,
+ A2A_DEFAULT_OUTPUT_MODES,
+ A2A_DEFAULT_TIMEOUT,
+ A2A_MAX_HISTORY_LENGTH,
+ A2A_PROTOCOL_VERSION,
+ A2A_TERMINAL_STATES,
+} from './constants'
+import { deliverPushNotification, notifyTaskStateChange } from './push-notifications'
+import type {
+ A2AAgentConfig,
+ A2AApiResponse,
+ A2ATaskRecord,
+ AgentAuthentication,
+ AgentCardSignature,
+ JSONSchema,
+} from './types'
+import {
+ buildA2AEndpointUrl,
+ buildAgentCardUrl,
+ createA2AToolId,
+ createAgentMessage,
+ createTextPart,
+ createUserMessage,
+ extractTextContent,
+ getLastAgentMessage,
+ getLastAgentMessageText,
+ isTerminalState,
+ parseA2AToolId,
+ sanitizeAgentName,
+} from './utils'
+
+export {
+ generateAgentCard,
+ generateDefaultAgentName,
+ generateSkillsFromWorkflow,
+ getAgentCardPaths,
+ mergeAgentCard,
+ validateAgentCard,
+ A2A_CACHE,
+ A2A_DEFAULT_CAPABILITIES,
+ A2A_DEFAULT_INPUT_MODES,
+ A2A_DEFAULT_OUTPUT_MODES,
+ A2A_DEFAULT_TIMEOUT,
+ A2A_MAX_HISTORY_LENGTH,
+ A2A_PROTOCOL_VERSION,
+ A2A_TERMINAL_STATES,
+ deliverPushNotification,
+ notifyTaskStateChange,
+ buildA2AEndpointUrl,
+ buildAgentCardUrl,
+ createA2AToolId,
+ createAgentMessage,
+ createTextPart,
+ createUserMessage,
+ extractTextContent,
+ getLastAgentMessage,
+ getLastAgentMessageText,
+ isTerminalState,
+ parseA2AToolId,
+ sanitizeAgentName,
+}
+
+export type {
+ AppAgentCard,
+ A2AAgentConfig,
+ A2AApiResponse,
+ A2ATaskRecord,
+ AgentAuthentication,
+ AgentCardSignature,
+ JSONSchema,
+}
diff --git a/apps/sim/lib/a2a/push-notifications.ts b/apps/sim/lib/a2a/push-notifications.ts
new file mode 100644
index 0000000000..74e39d8f7a
--- /dev/null
+++ b/apps/sim/lib/a2a/push-notifications.ts
@@ -0,0 +1,114 @@
+import type { Artifact, Message, TaskState } from '@a2a-js/sdk'
+import { db } from '@sim/db'
+import { a2aPushNotificationConfig, a2aTask } from '@sim/db/schema'
+import { createLogger } from '@sim/logger'
+import { eq } from 'drizzle-orm'
+import { isTriggerDevEnabled } from '@/lib/core/config/feature-flags'
+
+const logger = createLogger('A2APushNotifications')
+
+/**
+ * Deliver push notification for a task state change.
+ * Works without any external dependencies (DB-only).
+ *
+ * Note: Push notifications are best-effort delivery. Failed deliveries are logged
+ * for monitoring but not retried (unless trigger.dev is enabled for durable delivery).
+ * The webhook URL must use HTTPS (validated at configuration time).
+ * Tokens are stored in plaintext and sent as Bearer tokens for webhook validation.
+ */
+export async function deliverPushNotification(taskId: string, state: TaskState): Promise {
+ const [config] = await db
+ .select()
+ .from(a2aPushNotificationConfig)
+ .where(eq(a2aPushNotificationConfig.taskId, taskId))
+ .limit(1)
+
+ if (!config || !config.isActive) {
+ return true
+ }
+
+ const [task] = await db.select().from(a2aTask).where(eq(a2aTask.id, taskId)).limit(1)
+
+ if (!task) {
+ logger.warn('Task not found for push notification', { taskId })
+ return false
+ }
+
+ const timestamp = new Date().toISOString()
+
+ const headers: Record = {
+ 'Content-Type': 'application/json',
+ }
+
+ if (config.token) {
+ headers.Authorization = `Bearer ${config.token}`
+ }
+
+ try {
+ const response = await fetch(config.url, {
+ method: 'POST',
+ headers,
+ body: JSON.stringify({
+ kind: 'task-update',
+ task: {
+ kind: 'task',
+ id: task.id,
+ contextId: task.sessionId,
+ status: { state, timestamp },
+ history: task.messages as Message[],
+ artifacts: (task.artifacts as Artifact[]) || [],
+ },
+ }),
+ signal: AbortSignal.timeout(30000),
+ })
+
+ if (!response.ok) {
+ logger.error('Push notification delivery failed', {
+ taskId,
+ url: config.url,
+ status: response.status,
+ })
+ return false
+ }
+
+ logger.info('Push notification delivered successfully', { taskId, state })
+ return true
+ } catch (error) {
+ logger.error('Push notification delivery error', { taskId, error })
+ return false
+ }
+}
+
+/**
+ * Notify task state change.
+ * Uses trigger.dev for durable delivery when available, falls back to inline delivery.
+ */
+export async function notifyTaskStateChange(taskId: string, state: TaskState): Promise {
+ const [config] = await db
+ .select({ id: a2aPushNotificationConfig.id })
+ .from(a2aPushNotificationConfig)
+ .where(eq(a2aPushNotificationConfig.taskId, taskId))
+ .limit(1)
+
+ if (!config) {
+ return
+ }
+
+ if (isTriggerDevEnabled) {
+ try {
+ const { a2aPushNotificationTask } = await import(
+ '@/background/a2a-push-notification-delivery'
+ )
+ await a2aPushNotificationTask.trigger({ taskId, state })
+ logger.info('Push notification queued to trigger.dev', { taskId, state })
+ } catch (error) {
+ logger.warn('Failed to queue push notification, falling back to inline delivery', {
+ taskId,
+ error,
+ })
+ await deliverPushNotification(taskId, state)
+ }
+ } else {
+ await deliverPushNotification(taskId, state)
+ }
+}
diff --git a/apps/sim/lib/a2a/types.ts b/apps/sim/lib/a2a/types.ts
new file mode 100644
index 0000000000..c8c78ea60c
--- /dev/null
+++ b/apps/sim/lib/a2a/types.ts
@@ -0,0 +1,142 @@
+/**
+ * A2A (Agent-to-Agent) Protocol Types (v0.3)
+ * @see https://a2a-protocol.org/specification
+ */
+
+export {
+ AGENT_CARD_PATH,
+ type AgentCapabilities,
+ type AgentCard,
+ type AgentProvider,
+ type AgentSkill,
+ type Artifact,
+ type DataPart,
+ type FilePart,
+ type Message,
+ type MessageSendConfiguration,
+ type MessageSendParams,
+ type Part,
+ type PushNotificationConfig,
+ type Task,
+ type TaskArtifactUpdateEvent,
+ type TaskIdParams,
+ type TaskPushNotificationConfig,
+ type TaskQueryParams,
+ type TaskState,
+ type TaskStatus,
+ type TaskStatusUpdateEvent,
+ type TextPart,
+} from '@a2a-js/sdk'
+export {
+ type A2AClientOptions,
+ type AuthenticationHandler,
+ Client,
+ type ClientConfig,
+ ClientFactory,
+ type RequestOptions,
+} from '@a2a-js/sdk/client'
+export {
+ A2AError,
+ type AgentExecutor,
+ DefaultExecutionEventBus,
+ DefaultRequestHandler,
+ type ExecutionEventBus,
+ InMemoryTaskStore,
+ JsonRpcTransportHandler,
+ type RequestContext,
+ type TaskStore,
+} from '@a2a-js/sdk/server'
+
+/**
+ * App-specific: Extended MessageSendParams
+ * Note: Structured inputs should be passed via DataPart in message.parts (A2A spec compliant)
+ * Files should be passed via FilePart in message.parts
+ */
+export interface ExtendedMessageSendParams {
+ message: import('@a2a-js/sdk').Message
+ configuration?: import('@a2a-js/sdk').MessageSendConfiguration
+}
+
+/**
+ * App-specific: Database model for A2A Agent configuration
+ */
+export interface A2AAgentConfig {
+ id: string
+ workspaceId: string
+ workflowId: string
+ name: string
+ description?: string
+ version: string
+ capabilities: import('@a2a-js/sdk').AgentCapabilities
+ skills: import('@a2a-js/sdk').AgentSkill[]
+ authentication?: AgentAuthentication
+ signatures?: AgentCardSignature[]
+ isPublished: boolean
+ publishedAt?: Date
+ createdAt: Date
+ updatedAt: Date
+}
+
+/**
+ * App-specific: Agent authentication configuration
+ */
+export interface AgentAuthentication {
+ schemes: Array<'bearer' | 'apiKey' | 'oauth2' | 'none'>
+ credentials?: string
+}
+
+/**
+ * App-specific: Agent card signature (v0.3)
+ */
+export interface AgentCardSignature {
+ algorithm: string
+ keyId: string
+ value: string
+}
+
+/**
+ * App-specific: Database model for A2A Task record
+ */
+export interface A2ATaskRecord {
+ id: string
+ agentId: string
+ contextId?: string
+ status: import('@a2a-js/sdk').TaskState
+ history: import('@a2a-js/sdk').Message[]
+ artifacts?: import('@a2a-js/sdk').Artifact[]
+ executionId?: string
+ metadata?: Record
+ createdAt: Date
+ updatedAt: Date
+ completedAt?: Date
+}
+
+/**
+ * App-specific: A2A API Response wrapper
+ */
+export interface A2AApiResponse {
+ success: boolean
+ data?: T
+ error?: string
+}
+
+/**
+ * App-specific: JSON Schema definition for skill input/output schemas
+ */
+export interface JSONSchema {
+ type?: string
+ properties?: Record
+ items?: JSONSchema
+ required?: string[]
+ description?: string
+ enum?: unknown[]
+ default?: unknown
+ format?: string
+ minimum?: number
+ maximum?: number
+ minLength?: number
+ maxLength?: number
+ pattern?: string
+ additionalProperties?: boolean | JSONSchema
+ [key: string]: unknown
+}
diff --git a/apps/sim/lib/a2a/utils.ts b/apps/sim/lib/a2a/utils.ts
new file mode 100644
index 0000000000..119059d994
--- /dev/null
+++ b/apps/sim/lib/a2a/utils.ts
@@ -0,0 +1,286 @@
+import type { DataPart, FilePart, Message, Part, Task, TaskState, TextPart } from '@a2a-js/sdk'
+import {
+ type BeforeArgs,
+ type CallInterceptor,
+ type Client,
+ ClientFactory,
+ ClientFactoryOptions,
+} from '@a2a-js/sdk/client'
+import { createLogger } from '@sim/logger'
+import { A2A_TERMINAL_STATES } from './constants'
+
+const logger = createLogger('A2AUtils')
+
+/**
+ * Interceptor to add X-API-Key header to outgoing A2A requests
+ */
+class ApiKeyInterceptor implements CallInterceptor {
+ constructor(private apiKey: string) {}
+
+ before(args: BeforeArgs): Promise {
+ args.options = {
+ ...args.options,
+ serviceParameters: {
+ ...args.options?.serviceParameters,
+ 'X-API-Key': this.apiKey,
+ },
+ }
+ return Promise.resolve()
+ }
+
+ after(): Promise {
+ return Promise.resolve()
+ }
+}
+
+/**
+ * Create an A2A client from an agent URL with optional API key authentication
+ *
+ * The agent URL should be the full endpoint URL (e.g., /api/a2a/serve/{agentId}).
+ * We pass an empty path to createFromUrl so it uses the URL directly for agent card
+ * discovery (GET on the URL) instead of appending .well-known/agent-card.json.
+ */
+export async function createA2AClient(agentUrl: string, apiKey?: string): Promise {
+ const factoryOptions = apiKey
+ ? ClientFactoryOptions.createFrom(ClientFactoryOptions.default, {
+ clientConfig: {
+ interceptors: [new ApiKeyInterceptor(apiKey)],
+ },
+ })
+ : ClientFactoryOptions.default
+ const factory = new ClientFactory(factoryOptions)
+ return factory.createFromUrl(agentUrl, '')
+}
+
+export function isTerminalState(state: TaskState): boolean {
+ return (A2A_TERMINAL_STATES as readonly string[]).includes(state)
+}
+
+export function extractTextContent(message: Message): string {
+ return message.parts
+ .filter((part): part is TextPart => part.kind === 'text')
+ .map((part) => part.text)
+ .join('\n')
+}
+
+export function extractDataContent(message: Message): Record {
+ const dataParts = message.parts.filter((part): part is DataPart => part.kind === 'data')
+ return dataParts.reduce((acc, part) => ({ ...acc, ...part.data }), {})
+}
+
+export interface A2AFile {
+ name?: string
+ mimeType?: string
+ uri?: string
+ bytes?: string
+}
+
+export function extractFileContent(message: Message): A2AFile[] {
+ return message.parts
+ .filter((part): part is FilePart => part.kind === 'file')
+ .map((part) => {
+ const file = part.file as unknown as Record
+ const uri = (file.url as string) || (file.uri as string)
+ return {
+ name: file.name as string | undefined,
+ mimeType: file.mimeType as string | undefined,
+ ...(uri ? { uri } : {}),
+ ...(file.bytes ? { bytes: file.bytes as string } : {}),
+ }
+ })
+}
+
+export interface ExecutionFileInput {
+ type: 'file' | 'url'
+ data: string
+ name: string
+ mime?: string
+}
+
+/**
+ * Validate base64 string format
+ */
+function isValidBase64(str: string): boolean {
+ if (!str || str.length === 0) return false
+ return /^[A-Za-z0-9+/]*={0,2}$/.test(str)
+}
+
+/**
+ * Convert A2A FileParts to execution file format
+ * This format is then processed by processInputFileFields in the execute endpoint
+ * FileWithUri → type 'url', FileWithBytes → type 'file' with data URL
+ * Files without uri or bytes, or with invalid base64, are filtered out
+ */
+export function convertFilesToExecutionFormat(files: A2AFile[]): ExecutionFileInput[] {
+ return files
+ .filter((file) => {
+ // Skip files without content
+ if (!file.uri && !file.bytes) return false
+ // Validate base64 if bytes are provided
+ if (file.bytes && !isValidBase64(file.bytes)) return false
+ return true
+ })
+ .map((file) => {
+ if (file.uri) {
+ return {
+ type: 'url' as const,
+ data: file.uri,
+ name: file.name || 'file',
+ mime: file.mimeType,
+ }
+ }
+ const dataUrl = `data:${file.mimeType || 'application/octet-stream'};base64,${file.bytes}`
+ return {
+ type: 'file' as const,
+ data: dataUrl,
+ name: file.name || 'file',
+ mime: file.mimeType,
+ }
+ })
+}
+
+export interface WorkflowInput {
+ input: string
+ data?: Record
+ files?: ExecutionFileInput[]
+}
+
+export function extractWorkflowInput(message: Message): WorkflowInput | null {
+ const messageText = extractTextContent(message)
+ const dataContent = extractDataContent(message)
+ const fileContent = extractFileContent(message)
+ const files = convertFilesToExecutionFormat(fileContent)
+ const hasData = Object.keys(dataContent).length > 0
+
+ if (!messageText && !hasData && files.length === 0) {
+ return null
+ }
+
+ return {
+ input: messageText,
+ ...(hasData ? { data: dataContent } : {}),
+ ...(files.length > 0 ? { files } : {}),
+ }
+}
+
+export function createTextPart(text: string): Part {
+ return { kind: 'text', text }
+}
+
+export function createUserMessage(text: string): Message {
+ return {
+ kind: 'message',
+ messageId: crypto.randomUUID(),
+ role: 'user',
+ parts: [{ kind: 'text', text }],
+ }
+}
+
+export function createAgentMessage(text: string): Message {
+ return {
+ kind: 'message',
+ messageId: crypto.randomUUID(),
+ role: 'agent',
+ parts: [{ kind: 'text', text }],
+ }
+}
+
+export function createA2AToolId(agentId: string, skillId: string): string {
+ return `a2a:${agentId}:${skillId}`
+}
+
+export function parseA2AToolId(toolId: string): { agentId: string; skillId: string } | null {
+ const parts = toolId.split(':')
+ if (parts.length !== 3 || parts[0] !== 'a2a') {
+ return null
+ }
+ return { agentId: parts[1], skillId: parts[2] }
+}
+
+export function sanitizeAgentName(name: string): string {
+ return name
+ .toLowerCase()
+ .replace(/[^a-z0-9]+/g, '-')
+ .replace(/^-+|-+$/g, '')
+ .substring(0, 64)
+}
+
+export function buildA2AEndpointUrl(baseUrl: string, agentId: string): string {
+ const base = baseUrl.replace(/\/$/, '')
+ return `${base}/api/a2a/serve/${agentId}`
+}
+
+export function buildAgentCardUrl(baseUrl: string, agentId: string): string {
+ const base = baseUrl.replace(/\/$/, '')
+ return `${base}/api/a2a/agents/${agentId}`
+}
+
+export function getLastAgentMessage(task: Task): Message | undefined {
+ return task.history?.filter((m) => m.role === 'agent').pop()
+}
+
+export function getLastAgentMessageText(task: Task): string {
+ const message = getLastAgentMessage(task)
+ return message ? extractTextContent(message) : ''
+}
+
+export interface ParsedSSEChunk {
+ /** Incremental content from chunk events */
+ content: string
+ /** Final content if this chunk contains the final event */
+ finalContent?: string
+ /** Whether this chunk indicates the stream is done */
+ isDone: boolean
+}
+
+/**
+ * Parse workflow SSE chunk and extract clean content
+ *
+ * Workflow execute endpoint returns SSE in this format:
+ * - data: {"event":"chunk","data":{"content":"partial text"}}
+ * - data: {"event":"final","data":{"success":true,"output":{"content":"full text"}}}
+ * - data: "[DONE]"
+ *
+ * This function extracts the actual text content for A2A streaming
+ */
+export function parseWorkflowSSEChunk(chunk: string): ParsedSSEChunk {
+ const result: ParsedSSEChunk = {
+ content: '',
+ isDone: false,
+ }
+
+ const lines = chunk.split('\n')
+
+ for (const line of lines) {
+ const trimmed = line.trim()
+
+ if (!trimmed.startsWith('data:')) continue
+
+ const dataContent = trimmed.slice(5).trim()
+
+ if (dataContent === '"[DONE]"' || dataContent === '[DONE]') {
+ result.isDone = true
+ continue
+ }
+
+ try {
+ const parsed = JSON.parse(dataContent)
+
+ if (parsed.event === 'chunk' && parsed.data?.content) {
+ result.content += parsed.data.content
+ } else if (parsed.event === 'final' && parsed.data?.output?.content) {
+ result.finalContent = parsed.data.output.content
+ result.isDone = true
+ }
+ } catch {
+ // Only log if content looks like it should be JSON (starts with { or [)
+ if (dataContent.startsWith('{') || dataContent.startsWith('[')) {
+ logger.debug('Failed to parse SSE data as JSON', {
+ dataPreview: dataContent.substring(0, 100),
+ })
+ }
+ }
+ }
+
+ return result
+}
diff --git a/apps/sim/lib/auth/hybrid.ts b/apps/sim/lib/auth/hybrid.ts
index 90559f4ed2..d9183dc830 100644
--- a/apps/sim/lib/auth/hybrid.ts
+++ b/apps/sim/lib/auth/hybrid.ts
@@ -113,7 +113,7 @@ export async function checkHybridAuth(
}
}
- // 3. Try API key auth
+ // 3. Try API key auth (X-API-Key header only)
const apiKeyHeader = request.headers.get('x-api-key')
if (apiKeyHeader) {
const result = await authenticateApiKeyFromHeader(apiKeyHeader)
diff --git a/apps/sim/lib/copilot/prompts.ts b/apps/sim/lib/copilot/prompts.ts
index 744cb909bb..713e97d221 100644
--- a/apps/sim/lib/copilot/prompts.ts
+++ b/apps/sim/lib/copilot/prompts.ts
@@ -1 +1 @@
-export const AGENT_MODE_SYSTEM_PROMPT = `You are a helpful AI assistant for Sim Studio, a powerful workflow automation platform.`
+export const AGENT_MODE_SYSTEM_PROMPT = `You are a helpful AI assistant for Sim, a powerful workflow automation platform.`
diff --git a/apps/sim/lib/core/config/feature-flags.ts b/apps/sim/lib/core/config/feature-flags.ts
index 2a57e569da..8b8bf75411 100644
--- a/apps/sim/lib/core/config/feature-flags.ts
+++ b/apps/sim/lib/core/config/feature-flags.ts
@@ -1,7 +1,7 @@
/**
* Environment utility functions for consistent environment detection across the application
*/
-import { env, isFalsy, isTruthy } from './env'
+import { env, getEnv, isFalsy, isTruthy } from './env'
/**
* Is the application running in production mode
@@ -21,7 +21,9 @@ export const isTest = env.NODE_ENV === 'test'
/**
* Is this the hosted version of the application
*/
-export const isHosted = true
+export const isHosted =
+ getEnv('NEXT_PUBLIC_APP_URL') === 'https://www.sim.ai' ||
+ getEnv('NEXT_PUBLIC_APP_URL') === 'https://www.staging.sim.ai'
/**
* Is billing enforcement enabled
diff --git a/apps/sim/lib/core/rate-limiter/types.ts b/apps/sim/lib/core/rate-limiter/types.ts
index 0834461f9a..282ee09e03 100644
--- a/apps/sim/lib/core/rate-limiter/types.ts
+++ b/apps/sim/lib/core/rate-limiter/types.ts
@@ -1,15 +1,8 @@
import { env } from '@/lib/core/config/env'
+import type { CoreTriggerType } from '@/stores/logs/filters/types'
import type { TokenBucketConfig } from './storage'
-export type TriggerType =
- | 'api'
- | 'webhook'
- | 'schedule'
- | 'manual'
- | 'chat'
- | 'mcp'
- | 'form'
- | 'api-endpoint'
+export type TriggerType = CoreTriggerType | 'form' | 'api-endpoint'
export type RateLimitCounterType = 'sync' | 'async' | 'api-endpoint'
diff --git a/apps/sim/lib/execution/preprocessing.ts b/apps/sim/lib/execution/preprocessing.ts
index b1487828c8..8ff7afd8d9 100644
--- a/apps/sim/lib/execution/preprocessing.ts
+++ b/apps/sim/lib/execution/preprocessing.ts
@@ -7,6 +7,7 @@ import { getHighestPrioritySubscription } from '@/lib/billing/core/subscription'
import { RateLimiter } from '@/lib/core/rate-limiter/rate-limiter'
import { LoggingSession } from '@/lib/logs/execution/logging-session'
import { getWorkspaceBilledAccountUserId } from '@/lib/workspaces/utils'
+import type { CoreTriggerType } from '@/stores/logs/filters/types'
const logger = createLogger('ExecutionPreprocessing')
@@ -108,7 +109,7 @@ export interface PreprocessExecutionOptions {
// Required fields
workflowId: string
userId: string // The authenticated user ID
- triggerType: 'manual' | 'api' | 'webhook' | 'schedule' | 'chat' | 'mcp' | 'form'
+ triggerType: CoreTriggerType | 'form'
executionId: string
requestId: string
diff --git a/apps/sim/lib/logs/get-trigger-options.ts b/apps/sim/lib/logs/get-trigger-options.ts
index 0f05a077d8..fd704c7755 100644
--- a/apps/sim/lib/logs/get-trigger-options.ts
+++ b/apps/sim/lib/logs/get-trigger-options.ts
@@ -38,6 +38,7 @@ export function getTriggerOptions(): TriggerOption[] {
{ value: 'form', label: 'Form', color: '#06b6d4' },
{ value: 'webhook', label: 'Webhook', color: '#ea580c' },
{ value: 'mcp', label: 'MCP', color: '#dc2626' },
+ { value: 'a2a', label: 'A2A', color: '#14b8a6' },
]
for (const trigger of triggers) {
diff --git a/apps/sim/lib/webhooks/provider-subscriptions.ts b/apps/sim/lib/webhooks/provider-subscriptions.ts
index d7782a7ce3..1e031d921c 100644
--- a/apps/sim/lib/webhooks/provider-subscriptions.ts
+++ b/apps/sim/lib/webhooks/provider-subscriptions.ts
@@ -82,7 +82,6 @@ export async function createTeamsSubscription(
}
}
- // Always use NEXT_PUBLIC_APP_URL to ensure Microsoft Graph can reach the public endpoint
const notificationUrl = getNotificationUrl(webhook)
const resource = `/chats/${chatId}/messages`
diff --git a/apps/sim/lib/workflows/operations/deployment-utils.ts b/apps/sim/lib/workflows/operations/deployment-utils.ts
index 70c1f035cb..c0dce11aab 100644
--- a/apps/sim/lib/workflows/operations/deployment-utils.ts
+++ b/apps/sim/lib/workflows/operations/deployment-utils.ts
@@ -6,6 +6,47 @@ import { useWorkflowStore } from '@/stores/workflows/workflow/store'
const logger = createLogger('DeploymentUtils')
+export interface InputField {
+ name: string
+ type: string
+}
+
+/**
+ * Gets the input format from the Start block
+ * Returns an array of field definitions with name and type
+ */
+export function getStartBlockInputFormat(): InputField[] {
+ try {
+ const candidates = resolveStartCandidates(useWorkflowStore.getState().blocks, {
+ execution: 'api',
+ })
+
+ const targetCandidate =
+ candidates.find((candidate) => candidate.path === StartBlockPath.UNIFIED) ||
+ candidates.find((candidate) => candidate.path === StartBlockPath.SPLIT_API) ||
+ candidates.find((candidate) => candidate.path === StartBlockPath.SPLIT_INPUT) ||
+ candidates.find((candidate) => candidate.path === StartBlockPath.LEGACY_STARTER)
+
+ const targetBlock = targetCandidate?.block
+
+ if (targetBlock) {
+ const inputFormat = useSubBlockStore.getState().getValue(targetBlock.id, 'inputFormat')
+ if (inputFormat && Array.isArray(inputFormat)) {
+ return inputFormat
+ .map((field: { name?: string; type?: string }) => ({
+ name: field.name || '',
+ type: field.type || 'string',
+ }))
+ .filter((field) => field.name)
+ }
+ }
+ } catch (error) {
+ logger.warn('Error getting start block input format:', error)
+ }
+
+ return []
+}
+
/**
* Gets the input format example for a workflow's API deployment
* Returns the -d flag with example data if inputs exist, empty string otherwise
@@ -72,13 +113,11 @@ export function getInputFormatExample(
})
}
- // Add streaming parameters if enabled and outputs are selected
if (includeStreaming && selectedStreamingOutputs.length > 0) {
exampleData.stream = true
const convertedOutputs = selectedStreamingOutputs
.map((outputId) => {
- // If it starts with a UUID, convert to blockName.attribute format
if (startsWithUuid(outputId)) {
const underscoreIndex = outputId.indexOf('_')
if (underscoreIndex === -1) return null
@@ -86,25 +125,20 @@ export function getInputFormatExample(
const blockId = outputId.substring(0, underscoreIndex)
const attribute = outputId.substring(underscoreIndex + 1)
- // Find the block by ID and get its name
const block = blocks.find((b) => b.id === blockId)
if (block?.name) {
return `${normalizeName(block.name)}.${attribute}`
}
- // Block not found (deleted), return null to filter out
return null
}
- // Already in blockName.attribute format, verify the block exists
const parts = outputId.split('.')
if (parts.length >= 2) {
const blockName = parts[0]
- // Check if a block with this name exists
const block = blocks.find(
(b) => b.name && normalizeName(b.name) === normalizeName(blockName)
)
if (!block) {
- // Block not found (deleted), return null to filter out
return null
}
}
diff --git a/apps/sim/lib/workflows/operations/import-export.ts b/apps/sim/lib/workflows/operations/import-export.ts
index bfb96b63e8..d2fb95628c 100644
--- a/apps/sim/lib/workflows/operations/import-export.ts
+++ b/apps/sim/lib/workflows/operations/import-export.ts
@@ -36,10 +36,125 @@ export interface WorkspaceExportStructure {
folders: FolderExportData[]
}
-function sanitizePathSegment(name: string): string {
+/**
+ * Sanitizes a string for use as a path segment in a ZIP file.
+ */
+export function sanitizePathSegment(name: string): string {
return name.replace(/[^a-z0-9-_]/gi, '-')
}
+/**
+ * Downloads a file to the user's device.
+ */
+export function downloadFile(
+ content: Blob | string,
+ filename: string,
+ mimeType = 'application/json'
+): void {
+ try {
+ const blob = content instanceof Blob ? content : new Blob([content], { type: mimeType })
+ const url = URL.createObjectURL(blob)
+ const a = document.createElement('a')
+ a.href = url
+ a.download = filename
+ document.body.appendChild(a)
+ a.click()
+ document.body.removeChild(a)
+ URL.revokeObjectURL(url)
+ } catch (error) {
+ logger.error('Failed to download file:', error)
+ }
+}
+
+/**
+ * Fetches a workflow's state and variables for export.
+ * Returns null if the workflow cannot be fetched.
+ */
+export async function fetchWorkflowForExport(
+ workflowId: string,
+ workflowMeta: { name: string; description?: string; color?: string; folderId?: string | null }
+): Promise {
+ try {
+ const workflowResponse = await fetch(`/api/workflows/${workflowId}`)
+ if (!workflowResponse.ok) {
+ logger.error(`Failed to fetch workflow ${workflowId}`)
+ return null
+ }
+
+ const { data: workflowData } = await workflowResponse.json()
+ if (!workflowData?.state) {
+ logger.warn(`Workflow ${workflowId} has no state`)
+ return null
+ }
+
+ const variablesResponse = await fetch(`/api/workflows/${workflowId}/variables`)
+ let workflowVariables: Record | undefined
+ if (variablesResponse.ok) {
+ const variablesData = await variablesResponse.json()
+ workflowVariables = variablesData?.data
+ }
+
+ return {
+ workflow: {
+ id: workflowId,
+ name: workflowMeta.name,
+ description: workflowMeta.description,
+ color: workflowMeta.color,
+ folderId: workflowMeta.folderId,
+ },
+ state: workflowData.state,
+ variables: workflowVariables,
+ }
+ } catch (error) {
+ logger.error(`Failed to fetch workflow ${workflowId} for export:`, error)
+ return null
+ }
+}
+
+/**
+ * Exports a single workflow to a JSON string.
+ */
+export function exportWorkflowToJson(workflowData: WorkflowExportData): string {
+ const workflowState = {
+ ...workflowData.state,
+ metadata: {
+ name: workflowData.workflow.name,
+ description: workflowData.workflow.description,
+ color: workflowData.workflow.color,
+ exportedAt: new Date().toISOString(),
+ },
+ variables: workflowData.variables,
+ }
+
+ const exportState = sanitizeForExport(workflowState)
+ return JSON.stringify(exportState, null, 2)
+}
+
+/**
+ * Exports multiple workflows to a ZIP file.
+ * Workflows are placed at the root level (no folder structure).
+ */
+export async function exportWorkflowsToZip(workflows: WorkflowExportData[]): Promise {
+ const zip = new JSZip()
+ const seenFilenames = new Set()
+
+ for (const workflow of workflows) {
+ const jsonContent = exportWorkflowToJson(workflow)
+ const baseName = sanitizePathSegment(workflow.workflow.name)
+ let filename = `${baseName}.json`
+ let counter = 1
+
+ while (seenFilenames.has(filename.toLowerCase())) {
+ filename = `${baseName}-${counter}.json`
+ counter++
+ }
+ seenFilenames.add(filename.toLowerCase())
+ zip.file(filename, jsonContent)
+ }
+
+ return await zip.generateAsync({ type: 'blob' })
+}
+
function buildFolderPath(
folderId: string | null | undefined,
foldersMap: Map
@@ -105,6 +220,61 @@ export async function exportWorkspaceToZip(
return await zip.generateAsync({ type: 'blob' })
}
+/**
+ * Export a folder and its contents to a ZIP file.
+ * Preserves nested folder structure with paths relative to the exported folder.
+ *
+ * @param folderName - Name of the folder being exported
+ * @param workflows - Workflows to export (should be filtered to only those in the folder subtree)
+ * @param folders - Subfolders within the exported folder (parentId should be null for direct children)
+ */
+export async function exportFolderToZip(
+ folderName: string,
+ workflows: WorkflowExportData[],
+ folders: FolderExportData[]
+): Promise {
+ const zip = new JSZip()
+ const foldersMap = new Map(folders.map((f) => [f.id, f]))
+
+ const metadata = {
+ folder: {
+ name: folderName,
+ exportedAt: new Date().toISOString(),
+ },
+ folders: folders.map((f) => ({ id: f.id, name: f.name, parentId: f.parentId })),
+ }
+
+ zip.file('_folder.json', JSON.stringify(metadata, null, 2))
+
+ for (const workflow of workflows) {
+ try {
+ const workflowState = {
+ ...workflow.state,
+ metadata: {
+ name: workflow.workflow.name,
+ description: workflow.workflow.description,
+ color: workflow.workflow.color,
+ exportedAt: new Date().toISOString(),
+ },
+ variables: workflow.variables,
+ }
+
+ const exportState = sanitizeForExport(workflowState)
+ const sanitizedName = sanitizePathSegment(workflow.workflow.name)
+ const filename = `${sanitizedName}-${workflow.workflow.id}.json`
+
+ const folderPath = buildFolderPath(workflow.workflow.folderId, foldersMap)
+ const fullPath = folderPath ? `${folderPath}/${filename}` : filename
+
+ zip.file(fullPath, JSON.stringify(exportState, null, 2))
+ } catch (error) {
+ logger.error(`Failed to export workflow ${workflow.workflow.id}:`, error)
+ }
+ }
+
+ return await zip.generateAsync({ type: 'blob' })
+}
+
export interface ImportedWorkflow {
content: string
name: string
diff --git a/apps/sim/lib/workflows/utils.ts b/apps/sim/lib/workflows/utils.ts
index f3bf49ece3..d17744af67 100644
--- a/apps/sim/lib/workflows/utils.ts
+++ b/apps/sim/lib/workflows/utils.ts
@@ -1,17 +1,14 @@
import { db } from '@sim/db'
-import { permissions, userStats, workflow as workflowTable, workspace } from '@sim/db/schema'
+import { permissions, userStats, workflow as workflowTable } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
-import type { InferSelectModel } from 'drizzle-orm'
import { and, eq } from 'drizzle-orm'
import { NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
-import type { PermissionType } from '@/lib/workspaces/permissions/utils'
+import { getWorkspaceWithOwner, type PermissionType } from '@/lib/workspaces/permissions/utils'
import type { ExecutionResult } from '@/executor/types'
const logger = createLogger('WorkflowUtils')
-type WorkflowSelection = InferSelectModel
-
export async function getWorkflowById(id: string) {
const rows = await db.select().from(workflowTable).where(eq(workflowTable.id, id)).limit(1)
@@ -44,11 +41,7 @@ export async function getWorkflowAccessContext(
let workspacePermission: PermissionType | null = null
if (workflow.workspaceId) {
- const [workspaceRow] = await db
- .select({ ownerId: workspace.ownerId })
- .from(workspace)
- .where(eq(workspace.id, workflow.workspaceId))
- .limit(1)
+ const workspaceRow = await getWorkspaceWithOwner(workflow.workspaceId)
workspaceOwnerId = workspaceRow?.ownerId ?? null
@@ -147,7 +140,6 @@ export const workflowHasResponseBlock = (executionResult: ExecutionResult): bool
return responseBlock !== undefined
}
-// Create a HTTP response from response block
export const createHttpResponseFromBlock = (executionResult: ExecutionResult): NextResponse => {
const { data = {}, status = 200, headers = {} } = executionResult.output
diff --git a/apps/sim/lib/workspaces/permissions/utils.test.ts b/apps/sim/lib/workspaces/permissions/utils.test.ts
index 4ec22ce4bf..938937d222 100644
--- a/apps/sim/lib/workspaces/permissions/utils.test.ts
+++ b/apps/sim/lib/workspaces/permissions/utils.test.ts
@@ -40,11 +40,15 @@ vi.mock('drizzle-orm', () => drizzleOrmMock)
import { db } from '@sim/db'
import {
+ checkWorkspaceAccess,
getManageableWorkspaces,
getUserEntityPermissions,
getUsersWithPermissions,
+ getWorkspaceById,
+ getWorkspaceWithOwner,
hasAdminPermission,
hasWorkspaceAdminAccess,
+ workspaceExists,
} from '@/lib/workspaces/permissions/utils'
const mockDb = db as any
@@ -610,4 +614,209 @@ describe('Permission Utils', () => {
expect(result).toEqual([])
})
})
+
+ describe('getWorkspaceById', () => {
+ it.concurrent('should return workspace when it exists', async () => {
+ const chain = createMockChain([{ id: 'workspace123' }])
+ mockDb.select.mockReturnValue(chain)
+
+ const result = await getWorkspaceById('workspace123')
+
+ expect(result).toEqual({ id: 'workspace123' })
+ })
+
+ it.concurrent('should return null when workspace does not exist', async () => {
+ const chain = createMockChain([])
+ mockDb.select.mockReturnValue(chain)
+
+ const result = await getWorkspaceById('non-existent')
+
+ expect(result).toBeNull()
+ })
+
+ it.concurrent('should handle empty workspace ID', async () => {
+ const chain = createMockChain([])
+ mockDb.select.mockReturnValue(chain)
+
+ const result = await getWorkspaceById('')
+
+ expect(result).toBeNull()
+ })
+ })
+
+ describe('getWorkspaceWithOwner', () => {
+ it.concurrent('should return workspace with owner when it exists', async () => {
+ const chain = createMockChain([{ id: 'workspace123', ownerId: 'owner456' }])
+ mockDb.select.mockReturnValue(chain)
+
+ const result = await getWorkspaceWithOwner('workspace123')
+
+ expect(result).toEqual({ id: 'workspace123', ownerId: 'owner456' })
+ })
+
+ it.concurrent('should return null when workspace does not exist', async () => {
+ const chain = createMockChain([])
+ mockDb.select.mockReturnValue(chain)
+
+ const result = await getWorkspaceWithOwner('non-existent')
+
+ expect(result).toBeNull()
+ })
+
+ it.concurrent('should handle workspace with null owner ID', async () => {
+ const chain = createMockChain([{ id: 'workspace123', ownerId: null }])
+ mockDb.select.mockReturnValue(chain)
+
+ const result = await getWorkspaceWithOwner('workspace123')
+
+ expect(result).toEqual({ id: 'workspace123', ownerId: null })
+ })
+ })
+
+ describe('workspaceExists', () => {
+ it.concurrent('should return true when workspace exists', async () => {
+ const chain = createMockChain([{ id: 'workspace123' }])
+ mockDb.select.mockReturnValue(chain)
+
+ const result = await workspaceExists('workspace123')
+
+ expect(result).toBe(true)
+ })
+
+ it.concurrent('should return false when workspace does not exist', async () => {
+ const chain = createMockChain([])
+ mockDb.select.mockReturnValue(chain)
+
+ const result = await workspaceExists('non-existent')
+
+ expect(result).toBe(false)
+ })
+
+ it.concurrent('should handle empty workspace ID', async () => {
+ const chain = createMockChain([])
+ mockDb.select.mockReturnValue(chain)
+
+ const result = await workspaceExists('')
+
+ expect(result).toBe(false)
+ })
+ })
+
+ describe('checkWorkspaceAccess', () => {
+ it('should return exists=false when workspace does not exist', async () => {
+ const chain = createMockChain([])
+ mockDb.select.mockReturnValue(chain)
+
+ const result = await checkWorkspaceAccess('non-existent', 'user123')
+
+ expect(result).toEqual({
+ exists: false,
+ hasAccess: false,
+ canWrite: false,
+ workspace: null,
+ })
+ })
+
+ it('should return full access when user is workspace owner', async () => {
+ const chain = createMockChain([{ id: 'workspace123', ownerId: 'user123' }])
+ mockDb.select.mockReturnValue(chain)
+
+ const result = await checkWorkspaceAccess('workspace123', 'user123')
+
+ expect(result).toEqual({
+ exists: true,
+ hasAccess: true,
+ canWrite: true,
+ workspace: { id: 'workspace123', ownerId: 'user123' },
+ })
+ })
+
+ it('should return hasAccess=false when user has no permissions', async () => {
+ let callCount = 0
+ mockDb.select.mockImplementation(() => {
+ callCount++
+ if (callCount === 1) {
+ return createMockChain([{ id: 'workspace123', ownerId: 'other-user' }])
+ }
+ return createMockChain([]) // No permissions
+ })
+
+ const result = await checkWorkspaceAccess('workspace123', 'user123')
+
+ expect(result.exists).toBe(true)
+ expect(result.hasAccess).toBe(false)
+ expect(result.canWrite).toBe(false)
+ })
+
+ it('should return canWrite=true when user has admin permission', async () => {
+ let callCount = 0
+ mockDb.select.mockImplementation(() => {
+ callCount++
+ if (callCount === 1) {
+ return createMockChain([{ id: 'workspace123', ownerId: 'other-user' }])
+ }
+ return createMockChain([{ permissionType: 'admin' }])
+ })
+
+ const result = await checkWorkspaceAccess('workspace123', 'user123')
+
+ expect(result.exists).toBe(true)
+ expect(result.hasAccess).toBe(true)
+ expect(result.canWrite).toBe(true)
+ })
+
+ it('should return canWrite=true when user has write permission', async () => {
+ let callCount = 0
+ mockDb.select.mockImplementation(() => {
+ callCount++
+ if (callCount === 1) {
+ return createMockChain([{ id: 'workspace123', ownerId: 'other-user' }])
+ }
+ return createMockChain([{ permissionType: 'write' }])
+ })
+
+ const result = await checkWorkspaceAccess('workspace123', 'user123')
+
+ expect(result.exists).toBe(true)
+ expect(result.hasAccess).toBe(true)
+ expect(result.canWrite).toBe(true)
+ })
+
+ it('should return canWrite=false when user has read permission', async () => {
+ let callCount = 0
+ mockDb.select.mockImplementation(() => {
+ callCount++
+ if (callCount === 1) {
+ return createMockChain([{ id: 'workspace123', ownerId: 'other-user' }])
+ }
+ return createMockChain([{ permissionType: 'read' }])
+ })
+
+ const result = await checkWorkspaceAccess('workspace123', 'user123')
+
+ expect(result.exists).toBe(true)
+ expect(result.hasAccess).toBe(true)
+ expect(result.canWrite).toBe(false)
+ })
+
+ it('should handle empty user ID', async () => {
+ const chain = createMockChain([])
+ mockDb.select.mockReturnValue(chain)
+
+ const result = await checkWorkspaceAccess('workspace123', '')
+
+ expect(result.exists).toBe(false)
+ expect(result.hasAccess).toBe(false)
+ })
+
+ it('should handle empty workspace ID', async () => {
+ const chain = createMockChain([])
+ mockDb.select.mockReturnValue(chain)
+
+ const result = await checkWorkspaceAccess('', 'user123')
+
+ expect(result.exists).toBe(false)
+ expect(result.hasAccess).toBe(false)
+ })
+ })
})
diff --git a/apps/sim/lib/workspaces/permissions/utils.ts b/apps/sim/lib/workspaces/permissions/utils.ts
index c34b7840b0..a77ead9ca2 100644
--- a/apps/sim/lib/workspaces/permissions/utils.ts
+++ b/apps/sim/lib/workspaces/permissions/utils.ts
@@ -3,6 +3,112 @@ import { permissions, type permissionTypeEnum, user, workspace } from '@sim/db/s
import { and, eq } from 'drizzle-orm'
export type PermissionType = (typeof permissionTypeEnum.enumValues)[number]
+export interface WorkspaceBasic {
+ id: string
+}
+
+export interface WorkspaceWithOwner {
+ id: string
+ ownerId: string
+}
+
+export interface WorkspaceAccess {
+ exists: boolean
+ hasAccess: boolean
+ canWrite: boolean
+ workspace: WorkspaceWithOwner | null
+}
+
+/**
+ * Check if a workspace exists
+ *
+ * @param workspaceId - The workspace ID to check
+ * @returns True if the workspace exists, false otherwise
+ */
+export async function workspaceExists(workspaceId: string): Promise {
+ const [ws] = await db
+ .select({ id: workspace.id })
+ .from(workspace)
+ .where(eq(workspace.id, workspaceId))
+ .limit(1)
+
+ return !!ws
+}
+
+/**
+ * Get a workspace by ID for existence check
+ *
+ * @param workspaceId - The workspace ID to look up
+ * @returns The workspace if found, null otherwise
+ */
+export async function getWorkspaceById(workspaceId: string): Promise {
+ const exists = await workspaceExists(workspaceId)
+ return exists ? { id: workspaceId } : null
+}
+
+/**
+ * Get a workspace with owner info by ID
+ *
+ * @param workspaceId - The workspace ID to look up
+ * @returns The workspace with owner info if found, null otherwise
+ */
+export async function getWorkspaceWithOwner(
+ workspaceId: string
+): Promise {
+ const [ws] = await db
+ .select({ id: workspace.id, ownerId: workspace.ownerId })
+ .from(workspace)
+ .where(eq(workspace.id, workspaceId))
+ .limit(1)
+
+ return ws || null
+}
+
+/**
+ * Check workspace access for a user
+ *
+ * Verifies the workspace exists and the user has access to it.
+ * Returns access level (read/write) based on ownership and permissions.
+ *
+ * @param workspaceId - The workspace ID to check
+ * @param userId - The user ID to check access for
+ * @returns WorkspaceAccess object with exists, hasAccess, canWrite, and workspace data
+ */
+export async function checkWorkspaceAccess(
+ workspaceId: string,
+ userId: string
+): Promise {
+ const ws = await getWorkspaceWithOwner(workspaceId)
+
+ if (!ws) {
+ return { exists: false, hasAccess: false, canWrite: false, workspace: null }
+ }
+
+ if (ws.ownerId === userId) {
+ return { exists: true, hasAccess: true, canWrite: true, workspace: ws }
+ }
+
+ const [permissionRow] = await db
+ .select({ permissionType: permissions.permissionType })
+ .from(permissions)
+ .where(
+ and(
+ eq(permissions.userId, userId),
+ eq(permissions.entityType, 'workspace'),
+ eq(permissions.entityId, workspaceId)
+ )
+ )
+ .limit(1)
+
+ if (!permissionRow) {
+ return { exists: true, hasAccess: false, canWrite: false, workspace: ws }
+ }
+
+ const canWrite =
+ permissionRow.permissionType === 'write' || permissionRow.permissionType === 'admin'
+
+ return { exists: true, hasAccess: true, canWrite, workspace: ws }
+}
/**
* Get the highest permission level a user has for a specific entity
@@ -111,17 +217,13 @@ export async function hasWorkspaceAdminAccess(
userId: string,
workspaceId: string
): Promise {
- const workspaceResult = await db
- .select({ ownerId: workspace.ownerId })
- .from(workspace)
- .where(eq(workspace.id, workspaceId))
- .limit(1)
+ const ws = await getWorkspaceWithOwner(workspaceId)
- if (workspaceResult.length === 0) {
+ if (!ws) {
return false
}
- if (workspaceResult[0].ownerId === userId) {
+ if (ws.ownerId === userId) {
return true
}
diff --git a/apps/sim/package.json b/apps/sim/package.json
index 9dc7abd47c..8287aff388 100644
--- a/apps/sim/package.json
+++ b/apps/sim/package.json
@@ -23,6 +23,7 @@
"generate-docs": "bun run ../../scripts/generate-docs.ts"
},
"dependencies": {
+ "@a2a-js/sdk": "0.3.7",
"@anthropic-ai/sdk": "^0.39.0",
"@aws-sdk/client-bedrock-runtime": "3.940.0",
"@aws-sdk/client-dynamodb": "3.940.0",
diff --git a/apps/sim/stores/logs/filters/types.ts b/apps/sim/stores/logs/filters/types.ts
index c44ee3bafb..dde0bb9303 100644
--- a/apps/sim/stores/logs/filters/types.ts
+++ b/apps/sim/stores/logs/filters/types.ts
@@ -174,7 +174,15 @@ export type TimeRange =
export type LogLevel = 'error' | 'info' | 'running' | 'pending' | 'all' | (string & {})
/** Core trigger types for workflow execution */
-export const CORE_TRIGGER_TYPES = ['manual', 'api', 'schedule', 'chat', 'webhook', 'mcp'] as const
+export const CORE_TRIGGER_TYPES = [
+ 'manual',
+ 'api',
+ 'schedule',
+ 'chat',
+ 'webhook',
+ 'mcp',
+ 'a2a',
+] as const
export type CoreTriggerType = (typeof CORE_TRIGGER_TYPES)[number]
diff --git a/apps/sim/tools/a2a/cancel_task.ts b/apps/sim/tools/a2a/cancel_task.ts
new file mode 100644
index 0000000000..a43bccc587
--- /dev/null
+++ b/apps/sim/tools/a2a/cancel_task.ts
@@ -0,0 +1,55 @@
+import type { ToolConfig } from '@/tools/types'
+import type { A2ACancelTaskParams, A2ACancelTaskResponse } from './types'
+
+export const a2aCancelTaskTool: ToolConfig = {
+ id: 'a2a_cancel_task',
+ name: 'A2A Cancel Task',
+ description: 'Cancel a running A2A task.',
+ version: '1.0.0',
+
+ params: {
+ agentUrl: {
+ type: 'string',
+ required: true,
+ description: 'The A2A agent endpoint URL',
+ },
+ taskId: {
+ type: 'string',
+ required: true,
+ description: 'Task ID to cancel',
+ },
+ apiKey: {
+ type: 'string',
+ description: 'API key for authentication',
+ },
+ },
+
+ request: {
+ url: '/api/tools/a2a/cancel-task',
+ method: 'POST',
+ headers: () => ({
+ 'Content-Type': 'application/json',
+ }),
+ body: (params: A2ACancelTaskParams) => ({
+ agentUrl: params.agentUrl,
+ taskId: params.taskId,
+ apiKey: params.apiKey,
+ }),
+ },
+
+ transformResponse: async (response: Response) => {
+ const data = await response.json()
+ return data
+ },
+
+ outputs: {
+ cancelled: {
+ type: 'boolean',
+ description: 'Whether cancellation was successful',
+ },
+ state: {
+ type: 'string',
+ description: 'Task state after cancellation',
+ },
+ },
+}
diff --git a/apps/sim/tools/a2a/delete_push_notification.ts b/apps/sim/tools/a2a/delete_push_notification.ts
new file mode 100644
index 0000000000..186e9834bc
--- /dev/null
+++ b/apps/sim/tools/a2a/delete_push_notification.ts
@@ -0,0 +1,60 @@
+import type { ToolConfig } from '@/tools/types'
+import type { A2ADeletePushNotificationParams, A2ADeletePushNotificationResponse } from './types'
+
+export const a2aDeletePushNotificationTool: ToolConfig<
+ A2ADeletePushNotificationParams,
+ A2ADeletePushNotificationResponse
+> = {
+ id: 'a2a_delete_push_notification',
+ name: 'A2A Delete Push Notification',
+ description: 'Delete the push notification webhook configuration for a task.',
+ version: '1.0.0',
+
+ params: {
+ agentUrl: {
+ type: 'string',
+ required: true,
+ description: 'The A2A agent endpoint URL',
+ },
+ taskId: {
+ type: 'string',
+ required: true,
+ description: 'Task ID to delete notification config for',
+ },
+ pushNotificationConfigId: {
+ type: 'string',
+ description:
+ 'Push notification configuration ID to delete (optional - server can derive from taskId)',
+ },
+ apiKey: {
+ type: 'string',
+ description: 'API key for authentication',
+ },
+ },
+
+ request: {
+ url: '/api/tools/a2a/delete-push-notification',
+ method: 'POST',
+ headers: () => ({
+ 'Content-Type': 'application/json',
+ }),
+ body: (params) => ({
+ agentUrl: params.agentUrl,
+ taskId: params.taskId,
+ pushNotificationConfigId: params.pushNotificationConfigId,
+ apiKey: params.apiKey,
+ }),
+ },
+
+ transformResponse: async (response: Response) => {
+ const data = await response.json()
+ return data
+ },
+
+ outputs: {
+ success: {
+ type: 'boolean',
+ description: 'Whether deletion was successful',
+ },
+ },
+}
diff --git a/apps/sim/tools/a2a/get_agent_card.ts b/apps/sim/tools/a2a/get_agent_card.ts
new file mode 100644
index 0000000000..e6ee38795b
--- /dev/null
+++ b/apps/sim/tools/a2a/get_agent_card.ts
@@ -0,0 +1,73 @@
+import type { ToolConfig } from '@/tools/types'
+import type { A2AGetAgentCardParams, A2AGetAgentCardResponse } from './types'
+
+export const a2aGetAgentCardTool: ToolConfig = {
+ id: 'a2a_get_agent_card',
+ name: 'A2A Get Agent Card',
+ description: 'Fetch the Agent Card (discovery document) for an A2A agent.',
+ version: '1.0.0',
+
+ params: {
+ agentUrl: {
+ type: 'string',
+ required: true,
+ description: 'The A2A agent endpoint URL',
+ },
+ apiKey: {
+ type: 'string',
+ description: 'API key for authentication (if required)',
+ },
+ },
+
+ request: {
+ url: '/api/tools/a2a/get-agent-card',
+ method: 'POST',
+ headers: () => ({
+ 'Content-Type': 'application/json',
+ }),
+ body: (params) => ({
+ agentUrl: params.agentUrl,
+ apiKey: params.apiKey,
+ }),
+ },
+
+ transformResponse: async (response: Response) => {
+ const data = await response.json()
+ return data
+ },
+
+ outputs: {
+ name: {
+ type: 'string',
+ description: 'Agent name',
+ },
+ description: {
+ type: 'string',
+ description: 'Agent description',
+ },
+ url: {
+ type: 'string',
+ description: 'Agent endpoint URL',
+ },
+ version: {
+ type: 'string',
+ description: 'Agent version',
+ },
+ capabilities: {
+ type: 'object',
+ description: 'Agent capabilities (streaming, pushNotifications, etc.)',
+ },
+ skills: {
+ type: 'array',
+ description: 'Skills the agent can perform',
+ },
+ defaultInputModes: {
+ type: 'array',
+ description: 'Default input modes (text, file, data)',
+ },
+ defaultOutputModes: {
+ type: 'array',
+ description: 'Default output modes (text, file, data)',
+ },
+ },
+}
diff --git a/apps/sim/tools/a2a/get_push_notification.ts b/apps/sim/tools/a2a/get_push_notification.ts
new file mode 100644
index 0000000000..e117923674
--- /dev/null
+++ b/apps/sim/tools/a2a/get_push_notification.ts
@@ -0,0 +1,77 @@
+import type { ToolConfig } from '@/tools/types'
+import type { A2AGetPushNotificationParams, A2AGetPushNotificationResponse } from './types'
+
+export const a2aGetPushNotificationTool: ToolConfig<
+ A2AGetPushNotificationParams,
+ A2AGetPushNotificationResponse
+> = {
+ id: 'a2a_get_push_notification',
+ name: 'A2A Get Push Notification',
+ description: 'Get the push notification webhook configuration for a task.',
+ version: '1.0.0',
+
+ params: {
+ agentUrl: {
+ type: 'string',
+ required: true,
+ description: 'The A2A agent endpoint URL',
+ },
+ taskId: {
+ type: 'string',
+ required: true,
+ description: 'Task ID to get notification config for',
+ },
+ apiKey: {
+ type: 'string',
+ description: 'API key for authentication',
+ },
+ },
+
+ request: {
+ url: '/api/tools/a2a/get-push-notification',
+ method: 'POST',
+ headers: () => ({
+ 'Content-Type': 'application/json',
+ }),
+ body: (params) => ({
+ agentUrl: params.agentUrl,
+ taskId: params.taskId,
+ apiKey: params.apiKey,
+ }),
+ },
+
+ transformResponse: async (response: Response) => {
+ const data = await response.json()
+
+ if (!response.ok) {
+ return {
+ success: false,
+ output: {
+ exists: false,
+ },
+ error: data.error || 'Failed to get push notification',
+ }
+ }
+
+ return {
+ success: data.success,
+ output: data.output,
+ error: data.error,
+ }
+ },
+
+ outputs: {
+ url: {
+ type: 'string',
+ description: 'Configured webhook URL',
+ },
+ token: {
+ type: 'string',
+ description: 'Token for webhook validation',
+ },
+ exists: {
+ type: 'boolean',
+ description: 'Whether a push notification config exists',
+ },
+ },
+}
diff --git a/apps/sim/tools/a2a/get_task.ts b/apps/sim/tools/a2a/get_task.ts
new file mode 100644
index 0000000000..1c62b408f4
--- /dev/null
+++ b/apps/sim/tools/a2a/get_task.ts
@@ -0,0 +1,72 @@
+import type { ToolConfig } from '@/tools/types'
+import type { A2AGetTaskParams, A2AGetTaskResponse } from './types'
+
+export const a2aGetTaskTool: ToolConfig = {
+ id: 'a2a_get_task',
+ name: 'A2A Get Task',
+ description: 'Query the status of an existing A2A task.',
+ version: '1.0.0',
+
+ params: {
+ agentUrl: {
+ type: 'string',
+ required: true,
+ description: 'The A2A agent endpoint URL',
+ },
+ taskId: {
+ type: 'string',
+ required: true,
+ description: 'Task ID to query',
+ },
+ apiKey: {
+ type: 'string',
+ description: 'API key for authentication',
+ },
+ historyLength: {
+ type: 'number',
+ description: 'Number of history messages to include',
+ },
+ },
+
+ request: {
+ url: '/api/tools/a2a/get-task',
+ method: 'POST',
+ headers: () => ({
+ 'Content-Type': 'application/json',
+ }),
+ body: (params: A2AGetTaskParams) => ({
+ agentUrl: params.agentUrl,
+ taskId: params.taskId,
+ apiKey: params.apiKey,
+ historyLength: params.historyLength,
+ }),
+ },
+
+ transformResponse: async (response: Response) => {
+ const data = await response.json()
+ return data
+ },
+
+ outputs: {
+ taskId: {
+ type: 'string',
+ description: 'Task ID',
+ },
+ contextId: {
+ type: 'string',
+ description: 'Context ID',
+ },
+ state: {
+ type: 'string',
+ description: 'Task state',
+ },
+ artifacts: {
+ type: 'array',
+ description: 'Output artifacts',
+ },
+ history: {
+ type: 'array',
+ description: 'Message history',
+ },
+ },
+}
diff --git a/apps/sim/tools/a2a/index.ts b/apps/sim/tools/a2a/index.ts
new file mode 100644
index 0000000000..7b78f26424
--- /dev/null
+++ b/apps/sim/tools/a2a/index.ts
@@ -0,0 +1,21 @@
+import { a2aCancelTaskTool } from './cancel_task'
+import { a2aDeletePushNotificationTool } from './delete_push_notification'
+import { a2aGetAgentCardTool } from './get_agent_card'
+import { a2aGetPushNotificationTool } from './get_push_notification'
+import { a2aGetTaskTool } from './get_task'
+import { a2aResubscribeTool } from './resubscribe'
+import { a2aSendMessageTool } from './send_message'
+import { a2aSendMessageStreamTool } from './send_message_stream'
+import { a2aSetPushNotificationTool } from './set_push_notification'
+
+export {
+ a2aCancelTaskTool,
+ a2aDeletePushNotificationTool,
+ a2aGetAgentCardTool,
+ a2aGetPushNotificationTool,
+ a2aGetTaskTool,
+ a2aResubscribeTool,
+ a2aSendMessageTool,
+ a2aSendMessageStreamTool,
+ a2aSetPushNotificationTool,
+}
diff --git a/apps/sim/tools/a2a/resubscribe.ts b/apps/sim/tools/a2a/resubscribe.ts
new file mode 100644
index 0000000000..e2ed455855
--- /dev/null
+++ b/apps/sim/tools/a2a/resubscribe.ts
@@ -0,0 +1,87 @@
+import type { ToolConfig } from '@/tools/types'
+import type { A2AResubscribeParams, A2AResubscribeResponse } from './types'
+
+export const a2aResubscribeTool: ToolConfig = {
+ id: 'a2a_resubscribe',
+ name: 'A2A Resubscribe',
+ description: 'Reconnect to an ongoing A2A task stream after connection interruption.',
+ version: '1.0.0',
+
+ params: {
+ agentUrl: {
+ type: 'string',
+ required: true,
+ description: 'The A2A agent endpoint URL',
+ },
+ taskId: {
+ type: 'string',
+ required: true,
+ description: 'Task ID to resubscribe to',
+ },
+ apiKey: {
+ type: 'string',
+ description: 'API key for authentication',
+ },
+ },
+
+ request: {
+ url: '/api/tools/a2a/resubscribe',
+ method: 'POST',
+ headers: () => ({
+ 'Content-Type': 'application/json',
+ }),
+ body: (params: A2AResubscribeParams) => ({
+ agentUrl: params.agentUrl,
+ taskId: params.taskId,
+ apiKey: params.apiKey,
+ }),
+ },
+
+ transformResponse: async (response) => {
+ const data = await response.json()
+
+ if (!data.success) {
+ return {
+ success: false,
+ output: {
+ taskId: '',
+ state: 'failed' as const,
+ isRunning: false,
+ },
+ error: data.error || 'Failed to resubscribe',
+ }
+ }
+
+ return {
+ success: true,
+ output: data.output,
+ }
+ },
+
+ outputs: {
+ taskId: {
+ type: 'string',
+ description: 'Task ID',
+ },
+ contextId: {
+ type: 'string',
+ description: 'Context ID',
+ },
+ state: {
+ type: 'string',
+ description: 'Current task state',
+ },
+ isRunning: {
+ type: 'boolean',
+ description: 'Whether the task is still running',
+ },
+ artifacts: {
+ type: 'array',
+ description: 'Output artifacts',
+ },
+ history: {
+ type: 'array',
+ description: 'Message history',
+ },
+ },
+}
diff --git a/apps/sim/tools/a2a/send_message.ts b/apps/sim/tools/a2a/send_message.ts
new file mode 100644
index 0000000000..6da9cf11c8
--- /dev/null
+++ b/apps/sim/tools/a2a/send_message.ts
@@ -0,0 +1,72 @@
+import type { ToolConfig } from '@/tools/types'
+import type { A2ASendMessageParams, A2ASendMessageResponse } from './types'
+
+export const a2aSendMessageTool: ToolConfig = {
+ id: 'a2a_send_message',
+ name: 'A2A Send Message',
+ description: 'Send a message to an external A2A-compatible agent.',
+ version: '1.0.0',
+
+ params: {
+ agentUrl: {
+ type: 'string',
+ required: true,
+ description: 'The A2A agent endpoint URL',
+ },
+ message: {
+ type: 'string',
+ required: true,
+ description: 'Message to send to the agent',
+ },
+ taskId: {
+ type: 'string',
+ description: 'Task ID for continuing an existing task',
+ },
+ contextId: {
+ type: 'string',
+ description: 'Context ID for conversation continuity',
+ },
+ apiKey: {
+ type: 'string',
+ description: 'API key for authentication',
+ },
+ },
+
+ request: {
+ url: '/api/tools/a2a/send-message',
+ method: 'POST',
+ headers: () => ({}),
+ },
+
+ transformResponse: async (response: Response) => {
+ const data = await response.json()
+ return data
+ },
+
+ outputs: {
+ content: {
+ type: 'string',
+ description: 'The text response from the agent',
+ },
+ taskId: {
+ type: 'string',
+ description: 'Task ID for follow-up interactions',
+ },
+ contextId: {
+ type: 'string',
+ description: 'Context ID for conversation continuity',
+ },
+ state: {
+ type: 'string',
+ description: 'Task state',
+ },
+ artifacts: {
+ type: 'array',
+ description: 'Structured output artifacts',
+ },
+ history: {
+ type: 'array',
+ description: 'Full message history',
+ },
+ },
+}
diff --git a/apps/sim/tools/a2a/send_message_stream.ts b/apps/sim/tools/a2a/send_message_stream.ts
new file mode 100644
index 0000000000..dd44856b0c
--- /dev/null
+++ b/apps/sim/tools/a2a/send_message_stream.ts
@@ -0,0 +1,81 @@
+import type { ToolConfig } from '@/tools/types'
+import type { A2ASendMessageParams, A2ASendMessageResponse } from './types'
+
+export const a2aSendMessageStreamTool: ToolConfig = {
+ id: 'a2a_send_message_stream',
+ name: 'A2A Send Message (Streaming)',
+ description: 'Send a message to an external A2A-compatible agent with real-time streaming.',
+ version: '1.0.0',
+
+ params: {
+ agentUrl: {
+ type: 'string',
+ required: true,
+ description: 'The A2A agent endpoint URL',
+ },
+ message: {
+ type: 'string',
+ required: true,
+ description: 'Message to send to the agent',
+ },
+ taskId: {
+ type: 'string',
+ description: 'Task ID for continuing an existing task',
+ },
+ contextId: {
+ type: 'string',
+ description: 'Context ID for conversation continuity',
+ },
+ apiKey: {
+ type: 'string',
+ description: 'API key for authentication',
+ },
+ },
+
+ request: {
+ url: '/api/tools/a2a/send-message-stream',
+ method: 'POST',
+ headers: () => ({
+ 'Content-Type': 'application/json',
+ }),
+ body: (params) => ({
+ agentUrl: params.agentUrl,
+ message: params.message,
+ taskId: params.taskId,
+ contextId: params.contextId,
+ apiKey: params.apiKey,
+ }),
+ },
+
+ transformResponse: async (response: Response) => {
+ const data = await response.json()
+ return data
+ },
+
+ outputs: {
+ content: {
+ type: 'string',
+ description: 'The text response from the agent',
+ },
+ taskId: {
+ type: 'string',
+ description: 'Task ID for follow-up interactions',
+ },
+ contextId: {
+ type: 'string',
+ description: 'Context ID for conversation continuity',
+ },
+ state: {
+ type: 'string',
+ description: 'Task state',
+ },
+ artifacts: {
+ type: 'array',
+ description: 'Structured output artifacts',
+ },
+ history: {
+ type: 'array',
+ description: 'Full message history',
+ },
+ },
+}
diff --git a/apps/sim/tools/a2a/set_push_notification.ts b/apps/sim/tools/a2a/set_push_notification.ts
new file mode 100644
index 0000000000..a1a69ed40b
--- /dev/null
+++ b/apps/sim/tools/a2a/set_push_notification.ts
@@ -0,0 +1,92 @@
+import type { ToolConfig } from '@/tools/types'
+import type { A2ASetPushNotificationParams, A2ASetPushNotificationResponse } from './types'
+
+export const a2aSetPushNotificationTool: ToolConfig<
+ A2ASetPushNotificationParams,
+ A2ASetPushNotificationResponse
+> = {
+ id: 'a2a_set_push_notification',
+ name: 'A2A Set Push Notification',
+ description: 'Configure a webhook to receive task update notifications.',
+ version: '1.0.0',
+
+ params: {
+ agentUrl: {
+ type: 'string',
+ required: true,
+ description: 'The A2A agent endpoint URL',
+ },
+ taskId: {
+ type: 'string',
+ required: true,
+ description: 'Task ID to configure notifications for',
+ },
+ webhookUrl: {
+ type: 'string',
+ required: true,
+ description: 'HTTPS webhook URL to receive notifications',
+ },
+ token: {
+ type: 'string',
+ description: 'Token for webhook validation',
+ },
+ apiKey: {
+ type: 'string',
+ description: 'API key for authentication',
+ },
+ },
+
+ request: {
+ url: '/api/tools/a2a/set-push-notification',
+ method: 'POST',
+ headers: () => ({
+ 'Content-Type': 'application/json',
+ }),
+ body: (params: A2ASetPushNotificationParams) => ({
+ agentUrl: params.agentUrl,
+ taskId: params.taskId,
+ webhookUrl: params.webhookUrl,
+ token: params.token,
+ apiKey: params.apiKey,
+ }),
+ },
+
+ transformResponse: async (response: Response) => {
+ const data = await response.json()
+
+ if (!data.success) {
+ return {
+ success: false,
+ output: {
+ url: '',
+ success: false,
+ },
+ error: data.error || 'Failed to set push notification',
+ }
+ }
+
+ return {
+ success: true,
+ output: {
+ url: data.output.url,
+ token: data.output.token,
+ success: data.output.success,
+ },
+ }
+ },
+
+ outputs: {
+ url: {
+ type: 'string',
+ description: 'Configured webhook URL',
+ },
+ token: {
+ type: 'string',
+ description: 'Token for webhook validation',
+ },
+ success: {
+ type: 'boolean',
+ description: 'Whether configuration was successful',
+ },
+ },
+}
diff --git a/apps/sim/tools/a2a/types.ts b/apps/sim/tools/a2a/types.ts
new file mode 100644
index 0000000000..7230eb563a
--- /dev/null
+++ b/apps/sim/tools/a2a/types.ts
@@ -0,0 +1,135 @@
+import type { Artifact, Message, TaskState } from '@a2a-js/sdk'
+import type { ToolResponse } from '@/tools/types'
+
+export interface A2AGetAgentCardParams {
+ agentUrl: string
+ apiKey?: string
+}
+
+export interface A2AGetAgentCardResponse extends ToolResponse {
+ output: {
+ name: string
+ description?: string
+ url: string
+ version: string
+ capabilities?: {
+ streaming?: boolean
+ pushNotifications?: boolean
+ stateTransitionHistory?: boolean
+ }
+ skills?: Array<{
+ id: string
+ name: string
+ description?: string
+ }>
+ }
+}
+
+export interface A2ASendMessageParams {
+ agentUrl: string
+ message: string
+ taskId?: string
+ contextId?: string
+ apiKey?: string
+}
+
+export interface A2ASendMessageResponse extends ToolResponse {
+ output: {
+ content: string
+ taskId: string
+ contextId?: string
+ state: TaskState
+ artifacts?: Artifact[]
+ history?: Message[]
+ }
+}
+
+export interface A2AGetTaskParams {
+ agentUrl: string
+ taskId: string
+ apiKey?: string
+ historyLength?: number
+}
+
+export interface A2AGetTaskResponse extends ToolResponse {
+ output: {
+ taskId: string
+ contextId?: string
+ state: TaskState
+ artifacts?: Artifact[]
+ history?: Message[]
+ }
+}
+
+export interface A2ACancelTaskParams {
+ agentUrl: string
+ taskId: string
+ apiKey?: string
+}
+
+export interface A2ACancelTaskResponse extends ToolResponse {
+ output: {
+ cancelled: boolean
+ state: TaskState
+ }
+}
+
+export interface A2AResubscribeParams {
+ agentUrl: string
+ taskId: string
+ apiKey?: string
+}
+
+export interface A2AResubscribeResponse extends ToolResponse {
+ output: {
+ taskId: string
+ contextId?: string
+ state: TaskState
+ isRunning: boolean
+ artifacts?: Artifact[]
+ history?: Message[]
+ }
+}
+
+export interface A2ASetPushNotificationParams {
+ agentUrl: string
+ taskId: string
+ webhookUrl: string
+ token?: string
+ apiKey?: string
+}
+
+export interface A2ASetPushNotificationResponse extends ToolResponse {
+ output: {
+ url: string
+ token?: string
+ success: boolean
+ }
+}
+
+export interface A2AGetPushNotificationParams {
+ agentUrl: string
+ taskId: string
+ apiKey?: string
+}
+
+export interface A2AGetPushNotificationResponse extends ToolResponse {
+ output: {
+ url?: string
+ token?: string
+ exists: boolean
+ }
+}
+
+export interface A2ADeletePushNotificationParams {
+ agentUrl: string
+ taskId: string
+ pushNotificationConfigId?: string
+ apiKey?: string
+}
+
+export interface A2ADeletePushNotificationResponse extends ToolResponse {
+ output: {
+ success: boolean
+ }
+}
diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts
index 05bcb9f5a3..6570eea636 100644
--- a/apps/sim/tools/registry.ts
+++ b/apps/sim/tools/registry.ts
@@ -1,3 +1,14 @@
+import {
+ a2aCancelTaskTool,
+ a2aDeletePushNotificationTool,
+ a2aGetAgentCardTool,
+ a2aGetPushNotificationTool,
+ a2aGetTaskTool,
+ a2aResubscribeTool,
+ a2aSendMessageStreamTool,
+ a2aSendMessageTool,
+ a2aSetPushNotificationTool,
+} from '@/tools/a2a'
import {
ahrefsBacklinksStatsTool,
ahrefsBacklinksTool,
@@ -1523,6 +1534,15 @@ import { sqsSendTool } from './sqs'
// Registry of all available tools
export const tools: Record = {
+ a2a_cancel_task: a2aCancelTaskTool,
+ a2a_delete_push_notification: a2aDeletePushNotificationTool,
+ a2a_get_agent_card: a2aGetAgentCardTool,
+ a2a_get_push_notification: a2aGetPushNotificationTool,
+ a2a_get_task: a2aGetTaskTool,
+ a2a_resubscribe: a2aResubscribeTool,
+ a2a_send_message: a2aSendMessageTool,
+ a2a_send_message_stream: a2aSendMessageStreamTool,
+ a2a_set_push_notification: a2aSetPushNotificationTool,
arxiv_search: arxivSearchTool,
arxiv_get_paper: arxivGetPaperTool,
arxiv_get_author_papers: arxivGetAuthorPapersTool,
diff --git a/bun.lock b/bun.lock
index bbfe3d9a25..8b136165e7 100644
--- a/bun.lock
+++ b/bun.lock
@@ -53,6 +53,7 @@
"name": "sim",
"version": "0.1.0",
"dependencies": {
+ "@a2a-js/sdk": "0.3.7",
"@anthropic-ai/sdk": "^0.39.0",
"@aws-sdk/client-bedrock-runtime": "3.940.0",
"@aws-sdk/client-dynamodb": "3.940.0",
@@ -318,6 +319,8 @@
"react-dom": "19.2.1",
},
"packages": {
+ "@a2a-js/sdk": ["@a2a-js/sdk@0.3.7", "", { "dependencies": { "uuid": "^11.1.0" }, "peerDependencies": { "express": "^4.21.2 || ^5.1.0" }, "optionalPeers": ["express"] }, "sha512-1WBghkOjgiKt4rPNje8jlB9VateVQXqyjlc887bY/H8yM82Hlf0+5JW8zB98BPExKAplI5XqtXVH980J6vqi+w=="],
+
"@adobe/css-tools": ["@adobe/css-tools@4.4.4", "", {}, "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg=="],
"@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.56", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-XHJKu0Yvfu9SPzRfsAFESa+9T7f2YJY6TxykKMfRsAwpeWAiX/Gbx5J5uM15AzYC3Rw8tVP3oH+j7jEivENirQ=="],
diff --git a/docker-compose.ollama.yml b/docker-compose.ollama.yml
index b3bb574a9c..4f32929f92 100644
--- a/docker-compose.ollama.yml
+++ b/docker-compose.ollama.yml
@@ -1,7 +1,7 @@
name: sim-with-ollama
services:
- # Main Sim Studio Application
+ # Main Sim Application
simstudio:
build:
context: .
diff --git a/packages/db/migrations/0139_late_cargill.sql b/packages/db/migrations/0139_late_cargill.sql
new file mode 100644
index 0000000000..925af0c448
--- /dev/null
+++ b/packages/db/migrations/0139_late_cargill.sql
@@ -0,0 +1,61 @@
+CREATE TYPE "public"."a2a_task_status" AS ENUM('submitted', 'working', 'input-required', 'completed', 'failed', 'canceled', 'rejected', 'auth-required', 'unknown');--> statement-breakpoint
+CREATE TABLE "a2a_agent" (
+ "id" text PRIMARY KEY NOT NULL,
+ "workspace_id" text NOT NULL,
+ "workflow_id" text NOT NULL,
+ "created_by" text NOT NULL,
+ "name" text NOT NULL,
+ "description" text,
+ "version" text DEFAULT '1.0.0' NOT NULL,
+ "capabilities" jsonb DEFAULT '{}' NOT NULL,
+ "skills" jsonb DEFAULT '[]' NOT NULL,
+ "authentication" jsonb DEFAULT '{}' NOT NULL,
+ "signatures" jsonb DEFAULT '[]',
+ "is_published" boolean DEFAULT false NOT NULL,
+ "published_at" timestamp,
+ "created_at" timestamp DEFAULT now() NOT NULL,
+ "updated_at" timestamp DEFAULT now() NOT NULL
+);
+--> statement-breakpoint
+CREATE TABLE "a2a_push_notification_config" (
+ "id" text PRIMARY KEY NOT NULL,
+ "task_id" text NOT NULL,
+ "url" text NOT NULL,
+ "token" text,
+ "auth_schemes" jsonb DEFAULT '[]',
+ "auth_credentials" text,
+ "is_active" boolean DEFAULT true NOT NULL,
+ "created_at" timestamp DEFAULT now() NOT NULL,
+ "updated_at" timestamp DEFAULT now() NOT NULL
+);
+--> statement-breakpoint
+CREATE TABLE "a2a_task" (
+ "id" text PRIMARY KEY NOT NULL,
+ "agent_id" text NOT NULL,
+ "session_id" text,
+ "status" "a2a_task_status" DEFAULT 'submitted' NOT NULL,
+ "messages" jsonb DEFAULT '[]' NOT NULL,
+ "artifacts" jsonb DEFAULT '[]',
+ "execution_id" text,
+ "metadata" jsonb DEFAULT '{}',
+ "created_at" timestamp DEFAULT now() NOT NULL,
+ "updated_at" timestamp DEFAULT now() NOT NULL,
+ "completed_at" timestamp
+);
+--> statement-breakpoint
+ALTER TABLE "a2a_agent" ADD CONSTRAINT "a2a_agent_workspace_id_workspace_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspace"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "a2a_agent" ADD CONSTRAINT "a2a_agent_workflow_id_workflow_id_fk" FOREIGN KEY ("workflow_id") REFERENCES "public"."workflow"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "a2a_agent" ADD CONSTRAINT "a2a_agent_created_by_user_id_fk" FOREIGN KEY ("created_by") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "a2a_push_notification_config" ADD CONSTRAINT "a2a_push_notification_config_task_id_a2a_task_id_fk" FOREIGN KEY ("task_id") REFERENCES "public"."a2a_task"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "a2a_task" ADD CONSTRAINT "a2a_task_agent_id_a2a_agent_id_fk" FOREIGN KEY ("agent_id") REFERENCES "public"."a2a_agent"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+CREATE INDEX "a2a_agent_workspace_id_idx" ON "a2a_agent" USING btree ("workspace_id");--> statement-breakpoint
+CREATE INDEX "a2a_agent_workflow_id_idx" ON "a2a_agent" USING btree ("workflow_id");--> statement-breakpoint
+CREATE INDEX "a2a_agent_created_by_idx" ON "a2a_agent" USING btree ("created_by");--> statement-breakpoint
+CREATE UNIQUE INDEX "a2a_agent_workspace_workflow_unique" ON "a2a_agent" USING btree ("workspace_id","workflow_id");--> statement-breakpoint
+CREATE INDEX "a2a_push_notification_config_task_id_idx" ON "a2a_push_notification_config" USING btree ("task_id");--> statement-breakpoint
+CREATE UNIQUE INDEX "a2a_push_notification_config_task_unique" ON "a2a_push_notification_config" USING btree ("task_id");--> statement-breakpoint
+CREATE INDEX "a2a_task_agent_id_idx" ON "a2a_task" USING btree ("agent_id");--> statement-breakpoint
+CREATE INDEX "a2a_task_session_id_idx" ON "a2a_task" USING btree ("session_id");--> statement-breakpoint
+CREATE INDEX "a2a_task_status_idx" ON "a2a_task" USING btree ("status");--> statement-breakpoint
+CREATE INDEX "a2a_task_execution_id_idx" ON "a2a_task" USING btree ("execution_id");--> statement-breakpoint
+CREATE INDEX "a2a_task_created_at_idx" ON "a2a_task" USING btree ("created_at");
\ No newline at end of file
diff --git a/packages/db/migrations/meta/0139_snapshot.json b/packages/db/migrations/meta/0139_snapshot.json
new file mode 100644
index 0000000000..e0f0d7510c
--- /dev/null
+++ b/packages/db/migrations/meta/0139_snapshot.json
@@ -0,0 +1,10245 @@
+{
+ "id": "c895e678-5c08-44f9-b1be-b1f1021a6603",
+ "prevId": "2a4fdd67-34c3-4fcf-8cb3-32353fc89d3c",
+ "version": "7",
+ "dialect": "postgresql",
+ "tables": {
+ "public.a2a_agent": {
+ "name": "a2a_agent",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "version": {
+ "name": "version",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'1.0.0'"
+ },
+ "capabilities": {
+ "name": "capabilities",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'"
+ },
+ "skills": {
+ "name": "skills",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'"
+ },
+ "authentication": {
+ "name": "authentication",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'"
+ },
+ "signatures": {
+ "name": "signatures",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'[]'"
+ },
+ "is_published": {
+ "name": "is_published",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "published_at": {
+ "name": "published_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "a2a_agent_workspace_id_idx": {
+ "name": "a2a_agent_workspace_id_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "a2a_agent_workflow_id_idx": {
+ "name": "a2a_agent_workflow_id_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "a2a_agent_created_by_idx": {
+ "name": "a2a_agent_created_by_idx",
+ "columns": [
+ {
+ "expression": "created_by",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "a2a_agent_workspace_workflow_unique": {
+ "name": "a2a_agent_workspace_workflow_unique",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "a2a_agent_workspace_id_workspace_id_fk": {
+ "name": "a2a_agent_workspace_id_workspace_id_fk",
+ "tableFrom": "a2a_agent",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "a2a_agent_workflow_id_workflow_id_fk": {
+ "name": "a2a_agent_workflow_id_workflow_id_fk",
+ "tableFrom": "a2a_agent",
+ "tableTo": "workflow",
+ "columnsFrom": ["workflow_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "a2a_agent_created_by_user_id_fk": {
+ "name": "a2a_agent_created_by_user_id_fk",
+ "tableFrom": "a2a_agent",
+ "tableTo": "user",
+ "columnsFrom": ["created_by"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.a2a_push_notification_config": {
+ "name": "a2a_push_notification_config",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "task_id": {
+ "name": "task_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "url": {
+ "name": "url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "token": {
+ "name": "token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "auth_schemes": {
+ "name": "auth_schemes",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'[]'"
+ },
+ "auth_credentials": {
+ "name": "auth_credentials",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "is_active": {
+ "name": "is_active",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "a2a_push_notification_config_task_id_idx": {
+ "name": "a2a_push_notification_config_task_id_idx",
+ "columns": [
+ {
+ "expression": "task_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "a2a_push_notification_config_task_unique": {
+ "name": "a2a_push_notification_config_task_unique",
+ "columns": [
+ {
+ "expression": "task_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "a2a_push_notification_config_task_id_a2a_task_id_fk": {
+ "name": "a2a_push_notification_config_task_id_a2a_task_id_fk",
+ "tableFrom": "a2a_push_notification_config",
+ "tableTo": "a2a_task",
+ "columnsFrom": ["task_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.a2a_task": {
+ "name": "a2a_task",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "agent_id": {
+ "name": "agent_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "session_id": {
+ "name": "session_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "a2a_task_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'submitted'"
+ },
+ "messages": {
+ "name": "messages",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'"
+ },
+ "artifacts": {
+ "name": "artifacts",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'[]'"
+ },
+ "execution_id": {
+ "name": "execution_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'{}'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "completed_at": {
+ "name": "completed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "a2a_task_agent_id_idx": {
+ "name": "a2a_task_agent_id_idx",
+ "columns": [
+ {
+ "expression": "agent_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "a2a_task_session_id_idx": {
+ "name": "a2a_task_session_id_idx",
+ "columns": [
+ {
+ "expression": "session_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "a2a_task_status_idx": {
+ "name": "a2a_task_status_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "a2a_task_execution_id_idx": {
+ "name": "a2a_task_execution_id_idx",
+ "columns": [
+ {
+ "expression": "execution_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "a2a_task_created_at_idx": {
+ "name": "a2a_task_created_at_idx",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "a2a_task_agent_id_a2a_agent_id_fk": {
+ "name": "a2a_task_agent_id_a2a_agent_id_fk",
+ "tableFrom": "a2a_task",
+ "tableTo": "a2a_agent",
+ "columnsFrom": ["agent_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.account": {
+ "name": "account",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "account_id": {
+ "name": "account_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provider_id": {
+ "name": "provider_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "access_token": {
+ "name": "access_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "refresh_token": {
+ "name": "refresh_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "id_token": {
+ "name": "id_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "access_token_expires_at": {
+ "name": "access_token_expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "refresh_token_expires_at": {
+ "name": "refresh_token_expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "scope": {
+ "name": "scope",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "password": {
+ "name": "password",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "account_user_id_idx": {
+ "name": "account_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_account_on_account_id_provider_id": {
+ "name": "idx_account_on_account_id_provider_id",
+ "columns": [
+ {
+ "expression": "account_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "provider_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "account_user_provider_account_unique": {
+ "name": "account_user_provider_account_unique",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "provider_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "account_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "account_user_id_user_id_fk": {
+ "name": "account_user_id_user_id_fk",
+ "tableFrom": "account",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.api_key": {
+ "name": "api_key",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "key": {
+ "name": "key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'personal'"
+ },
+ "last_used": {
+ "name": "last_used",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "api_key_workspace_type_idx": {
+ "name": "api_key_workspace_type_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "api_key_user_type_idx": {
+ "name": "api_key_user_type_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "api_key_user_id_user_id_fk": {
+ "name": "api_key_user_id_user_id_fk",
+ "tableFrom": "api_key",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "api_key_workspace_id_workspace_id_fk": {
+ "name": "api_key_workspace_id_workspace_id_fk",
+ "tableFrom": "api_key",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "api_key_created_by_user_id_fk": {
+ "name": "api_key_created_by_user_id_fk",
+ "tableFrom": "api_key",
+ "tableTo": "user",
+ "columnsFrom": ["created_by"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "api_key_key_unique": {
+ "name": "api_key_key_unique",
+ "nullsNotDistinct": false,
+ "columns": ["key"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {
+ "workspace_type_check": {
+ "name": "workspace_type_check",
+ "value": "(type = 'workspace' AND workspace_id IS NOT NULL) OR (type = 'personal' AND workspace_id IS NULL)"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.chat": {
+ "name": "chat",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "identifier": {
+ "name": "identifier",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "is_active": {
+ "name": "is_active",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "customizations": {
+ "name": "customizations",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'{}'"
+ },
+ "auth_type": {
+ "name": "auth_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'public'"
+ },
+ "password": {
+ "name": "password",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "allowed_emails": {
+ "name": "allowed_emails",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'[]'"
+ },
+ "output_configs": {
+ "name": "output_configs",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'[]'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "identifier_idx": {
+ "name": "identifier_idx",
+ "columns": [
+ {
+ "expression": "identifier",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "chat_workflow_id_workflow_id_fk": {
+ "name": "chat_workflow_id_workflow_id_fk",
+ "tableFrom": "chat",
+ "tableTo": "workflow",
+ "columnsFrom": ["workflow_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "chat_user_id_user_id_fk": {
+ "name": "chat_user_id_user_id_fk",
+ "tableFrom": "chat",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.copilot_chats": {
+ "name": "copilot_chats",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "messages": {
+ "name": "messages",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'"
+ },
+ "model": {
+ "name": "model",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'claude-3-7-sonnet-latest'"
+ },
+ "conversation_id": {
+ "name": "conversation_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "preview_yaml": {
+ "name": "preview_yaml",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "plan_artifact": {
+ "name": "plan_artifact",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "config": {
+ "name": "config",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "copilot_chats_user_id_idx": {
+ "name": "copilot_chats_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "copilot_chats_workflow_id_idx": {
+ "name": "copilot_chats_workflow_id_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "copilot_chats_user_workflow_idx": {
+ "name": "copilot_chats_user_workflow_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "copilot_chats_created_at_idx": {
+ "name": "copilot_chats_created_at_idx",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "copilot_chats_updated_at_idx": {
+ "name": "copilot_chats_updated_at_idx",
+ "columns": [
+ {
+ "expression": "updated_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "copilot_chats_user_id_user_id_fk": {
+ "name": "copilot_chats_user_id_user_id_fk",
+ "tableFrom": "copilot_chats",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "copilot_chats_workflow_id_workflow_id_fk": {
+ "name": "copilot_chats_workflow_id_workflow_id_fk",
+ "tableFrom": "copilot_chats",
+ "tableTo": "workflow",
+ "columnsFrom": ["workflow_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.copilot_feedback": {
+ "name": "copilot_feedback",
+ "schema": "",
+ "columns": {
+ "feedback_id": {
+ "name": "feedback_id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "chat_id": {
+ "name": "chat_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_query": {
+ "name": "user_query",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "agent_response": {
+ "name": "agent_response",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "is_positive": {
+ "name": "is_positive",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "feedback": {
+ "name": "feedback",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "workflow_yaml": {
+ "name": "workflow_yaml",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "copilot_feedback_user_id_idx": {
+ "name": "copilot_feedback_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "copilot_feedback_chat_id_idx": {
+ "name": "copilot_feedback_chat_id_idx",
+ "columns": [
+ {
+ "expression": "chat_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "copilot_feedback_user_chat_idx": {
+ "name": "copilot_feedback_user_chat_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "chat_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "copilot_feedback_is_positive_idx": {
+ "name": "copilot_feedback_is_positive_idx",
+ "columns": [
+ {
+ "expression": "is_positive",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "copilot_feedback_created_at_idx": {
+ "name": "copilot_feedback_created_at_idx",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "copilot_feedback_user_id_user_id_fk": {
+ "name": "copilot_feedback_user_id_user_id_fk",
+ "tableFrom": "copilot_feedback",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "copilot_feedback_chat_id_copilot_chats_id_fk": {
+ "name": "copilot_feedback_chat_id_copilot_chats_id_fk",
+ "tableFrom": "copilot_feedback",
+ "tableTo": "copilot_chats",
+ "columnsFrom": ["chat_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.credential_set": {
+ "name": "credential_set",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "provider_id": {
+ "name": "provider_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "credential_set_organization_id_idx": {
+ "name": "credential_set_organization_id_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "credential_set_created_by_idx": {
+ "name": "credential_set_created_by_idx",
+ "columns": [
+ {
+ "expression": "created_by",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "credential_set_org_name_unique": {
+ "name": "credential_set_org_name_unique",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "credential_set_provider_id_idx": {
+ "name": "credential_set_provider_id_idx",
+ "columns": [
+ {
+ "expression": "provider_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "credential_set_organization_id_organization_id_fk": {
+ "name": "credential_set_organization_id_organization_id_fk",
+ "tableFrom": "credential_set",
+ "tableTo": "organization",
+ "columnsFrom": ["organization_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "credential_set_created_by_user_id_fk": {
+ "name": "credential_set_created_by_user_id_fk",
+ "tableFrom": "credential_set",
+ "tableTo": "user",
+ "columnsFrom": ["created_by"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.credential_set_invitation": {
+ "name": "credential_set_invitation",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "credential_set_id": {
+ "name": "credential_set_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "token": {
+ "name": "token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "invited_by": {
+ "name": "invited_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "credential_set_invitation_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "accepted_at": {
+ "name": "accepted_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "accepted_by_user_id": {
+ "name": "accepted_by_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "credential_set_invitation_set_id_idx": {
+ "name": "credential_set_invitation_set_id_idx",
+ "columns": [
+ {
+ "expression": "credential_set_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "credential_set_invitation_token_idx": {
+ "name": "credential_set_invitation_token_idx",
+ "columns": [
+ {
+ "expression": "token",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "credential_set_invitation_status_idx": {
+ "name": "credential_set_invitation_status_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "credential_set_invitation_expires_at_idx": {
+ "name": "credential_set_invitation_expires_at_idx",
+ "columns": [
+ {
+ "expression": "expires_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "credential_set_invitation_credential_set_id_credential_set_id_fk": {
+ "name": "credential_set_invitation_credential_set_id_credential_set_id_fk",
+ "tableFrom": "credential_set_invitation",
+ "tableTo": "credential_set",
+ "columnsFrom": ["credential_set_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "credential_set_invitation_invited_by_user_id_fk": {
+ "name": "credential_set_invitation_invited_by_user_id_fk",
+ "tableFrom": "credential_set_invitation",
+ "tableTo": "user",
+ "columnsFrom": ["invited_by"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "credential_set_invitation_accepted_by_user_id_user_id_fk": {
+ "name": "credential_set_invitation_accepted_by_user_id_user_id_fk",
+ "tableFrom": "credential_set_invitation",
+ "tableTo": "user",
+ "columnsFrom": ["accepted_by_user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "credential_set_invitation_token_unique": {
+ "name": "credential_set_invitation_token_unique",
+ "nullsNotDistinct": false,
+ "columns": ["token"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.credential_set_member": {
+ "name": "credential_set_member",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "credential_set_id": {
+ "name": "credential_set_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "credential_set_member_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "joined_at": {
+ "name": "joined_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "invited_by": {
+ "name": "invited_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "credential_set_member_set_id_idx": {
+ "name": "credential_set_member_set_id_idx",
+ "columns": [
+ {
+ "expression": "credential_set_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "credential_set_member_user_id_idx": {
+ "name": "credential_set_member_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "credential_set_member_unique": {
+ "name": "credential_set_member_unique",
+ "columns": [
+ {
+ "expression": "credential_set_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "credential_set_member_status_idx": {
+ "name": "credential_set_member_status_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "credential_set_member_credential_set_id_credential_set_id_fk": {
+ "name": "credential_set_member_credential_set_id_credential_set_id_fk",
+ "tableFrom": "credential_set_member",
+ "tableTo": "credential_set",
+ "columnsFrom": ["credential_set_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "credential_set_member_user_id_user_id_fk": {
+ "name": "credential_set_member_user_id_user_id_fk",
+ "tableFrom": "credential_set_member",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "credential_set_member_invited_by_user_id_fk": {
+ "name": "credential_set_member_invited_by_user_id_fk",
+ "tableFrom": "credential_set_member",
+ "tableTo": "user",
+ "columnsFrom": ["invited_by"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.custom_tools": {
+ "name": "custom_tools",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "schema": {
+ "name": "schema",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "code": {
+ "name": "code",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "custom_tools_workspace_id_idx": {
+ "name": "custom_tools_workspace_id_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "custom_tools_workspace_title_unique": {
+ "name": "custom_tools_workspace_title_unique",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "title",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "custom_tools_workspace_id_workspace_id_fk": {
+ "name": "custom_tools_workspace_id_workspace_id_fk",
+ "tableFrom": "custom_tools",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "custom_tools_user_id_user_id_fk": {
+ "name": "custom_tools_user_id_user_id_fk",
+ "tableFrom": "custom_tools",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.docs_embeddings": {
+ "name": "docs_embeddings",
+ "schema": "",
+ "columns": {
+ "chunk_id": {
+ "name": "chunk_id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "chunk_text": {
+ "name": "chunk_text",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "source_document": {
+ "name": "source_document",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "source_link": {
+ "name": "source_link",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "header_text": {
+ "name": "header_text",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "header_level": {
+ "name": "header_level",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "token_count": {
+ "name": "token_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "embedding": {
+ "name": "embedding",
+ "type": "vector(1536)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "embedding_model": {
+ "name": "embedding_model",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'text-embedding-3-small'"
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'"
+ },
+ "chunk_text_tsv": {
+ "name": "chunk_text_tsv",
+ "type": "tsvector",
+ "primaryKey": false,
+ "notNull": false,
+ "generated": {
+ "as": "to_tsvector('english', \"docs_embeddings\".\"chunk_text\")",
+ "type": "stored"
+ }
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "docs_emb_source_document_idx": {
+ "name": "docs_emb_source_document_idx",
+ "columns": [
+ {
+ "expression": "source_document",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "docs_emb_header_level_idx": {
+ "name": "docs_emb_header_level_idx",
+ "columns": [
+ {
+ "expression": "header_level",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "docs_emb_source_header_idx": {
+ "name": "docs_emb_source_header_idx",
+ "columns": [
+ {
+ "expression": "source_document",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "header_level",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "docs_emb_model_idx": {
+ "name": "docs_emb_model_idx",
+ "columns": [
+ {
+ "expression": "embedding_model",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "docs_emb_created_at_idx": {
+ "name": "docs_emb_created_at_idx",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "docs_embedding_vector_hnsw_idx": {
+ "name": "docs_embedding_vector_hnsw_idx",
+ "columns": [
+ {
+ "expression": "embedding",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last",
+ "opclass": "vector_cosine_ops"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "hnsw",
+ "with": {
+ "m": 16,
+ "ef_construction": 64
+ }
+ },
+ "docs_emb_metadata_gin_idx": {
+ "name": "docs_emb_metadata_gin_idx",
+ "columns": [
+ {
+ "expression": "metadata",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "gin",
+ "with": {}
+ },
+ "docs_emb_chunk_text_fts_idx": {
+ "name": "docs_emb_chunk_text_fts_idx",
+ "columns": [
+ {
+ "expression": "chunk_text_tsv",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "gin",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "docs_embedding_not_null_check": {
+ "name": "docs_embedding_not_null_check",
+ "value": "\"embedding\" IS NOT NULL"
+ },
+ "docs_header_level_check": {
+ "name": "docs_header_level_check",
+ "value": "\"header_level\" >= 1 AND \"header_level\" <= 6"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.document": {
+ "name": "document",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "knowledge_base_id": {
+ "name": "knowledge_base_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "filename": {
+ "name": "filename",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "file_url": {
+ "name": "file_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "file_size": {
+ "name": "file_size",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "mime_type": {
+ "name": "mime_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "chunk_count": {
+ "name": "chunk_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "token_count": {
+ "name": "token_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "character_count": {
+ "name": "character_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "processing_status": {
+ "name": "processing_status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "processing_started_at": {
+ "name": "processing_started_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "processing_completed_at": {
+ "name": "processing_completed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "processing_error": {
+ "name": "processing_error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tag1": {
+ "name": "tag1",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tag2": {
+ "name": "tag2",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tag3": {
+ "name": "tag3",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tag4": {
+ "name": "tag4",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tag5": {
+ "name": "tag5",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tag6": {
+ "name": "tag6",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tag7": {
+ "name": "tag7",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "number1": {
+ "name": "number1",
+ "type": "double precision",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "number2": {
+ "name": "number2",
+ "type": "double precision",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "number3": {
+ "name": "number3",
+ "type": "double precision",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "number4": {
+ "name": "number4",
+ "type": "double precision",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "number5": {
+ "name": "number5",
+ "type": "double precision",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "date1": {
+ "name": "date1",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "date2": {
+ "name": "date2",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "boolean1": {
+ "name": "boolean1",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "boolean2": {
+ "name": "boolean2",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "boolean3": {
+ "name": "boolean3",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "uploaded_at": {
+ "name": "uploaded_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "doc_kb_id_idx": {
+ "name": "doc_kb_id_idx",
+ "columns": [
+ {
+ "expression": "knowledge_base_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_filename_idx": {
+ "name": "doc_filename_idx",
+ "columns": [
+ {
+ "expression": "filename",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_processing_status_idx": {
+ "name": "doc_processing_status_idx",
+ "columns": [
+ {
+ "expression": "knowledge_base_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "processing_status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_tag1_idx": {
+ "name": "doc_tag1_idx",
+ "columns": [
+ {
+ "expression": "tag1",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_tag2_idx": {
+ "name": "doc_tag2_idx",
+ "columns": [
+ {
+ "expression": "tag2",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_tag3_idx": {
+ "name": "doc_tag3_idx",
+ "columns": [
+ {
+ "expression": "tag3",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_tag4_idx": {
+ "name": "doc_tag4_idx",
+ "columns": [
+ {
+ "expression": "tag4",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_tag5_idx": {
+ "name": "doc_tag5_idx",
+ "columns": [
+ {
+ "expression": "tag5",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_tag6_idx": {
+ "name": "doc_tag6_idx",
+ "columns": [
+ {
+ "expression": "tag6",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_tag7_idx": {
+ "name": "doc_tag7_idx",
+ "columns": [
+ {
+ "expression": "tag7",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_number1_idx": {
+ "name": "doc_number1_idx",
+ "columns": [
+ {
+ "expression": "number1",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_number2_idx": {
+ "name": "doc_number2_idx",
+ "columns": [
+ {
+ "expression": "number2",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_number3_idx": {
+ "name": "doc_number3_idx",
+ "columns": [
+ {
+ "expression": "number3",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_number4_idx": {
+ "name": "doc_number4_idx",
+ "columns": [
+ {
+ "expression": "number4",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_number5_idx": {
+ "name": "doc_number5_idx",
+ "columns": [
+ {
+ "expression": "number5",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_date1_idx": {
+ "name": "doc_date1_idx",
+ "columns": [
+ {
+ "expression": "date1",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_date2_idx": {
+ "name": "doc_date2_idx",
+ "columns": [
+ {
+ "expression": "date2",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_boolean1_idx": {
+ "name": "doc_boolean1_idx",
+ "columns": [
+ {
+ "expression": "boolean1",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_boolean2_idx": {
+ "name": "doc_boolean2_idx",
+ "columns": [
+ {
+ "expression": "boolean2",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_boolean3_idx": {
+ "name": "doc_boolean3_idx",
+ "columns": [
+ {
+ "expression": "boolean3",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "document_knowledge_base_id_knowledge_base_id_fk": {
+ "name": "document_knowledge_base_id_knowledge_base_id_fk",
+ "tableFrom": "document",
+ "tableTo": "knowledge_base",
+ "columnsFrom": ["knowledge_base_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.embedding": {
+ "name": "embedding",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "knowledge_base_id": {
+ "name": "knowledge_base_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "document_id": {
+ "name": "document_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "chunk_index": {
+ "name": "chunk_index",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "chunk_hash": {
+ "name": "chunk_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "content": {
+ "name": "content",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "content_length": {
+ "name": "content_length",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "token_count": {
+ "name": "token_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "embedding": {
+ "name": "embedding",
+ "type": "vector(1536)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "embedding_model": {
+ "name": "embedding_model",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'text-embedding-3-small'"
+ },
+ "start_offset": {
+ "name": "start_offset",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "end_offset": {
+ "name": "end_offset",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "tag1": {
+ "name": "tag1",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tag2": {
+ "name": "tag2",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tag3": {
+ "name": "tag3",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tag4": {
+ "name": "tag4",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tag5": {
+ "name": "tag5",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tag6": {
+ "name": "tag6",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tag7": {
+ "name": "tag7",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "number1": {
+ "name": "number1",
+ "type": "double precision",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "number2": {
+ "name": "number2",
+ "type": "double precision",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "number3": {
+ "name": "number3",
+ "type": "double precision",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "number4": {
+ "name": "number4",
+ "type": "double precision",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "number5": {
+ "name": "number5",
+ "type": "double precision",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "date1": {
+ "name": "date1",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "date2": {
+ "name": "date2",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "boolean1": {
+ "name": "boolean1",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "boolean2": {
+ "name": "boolean2",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "boolean3": {
+ "name": "boolean3",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "content_tsv": {
+ "name": "content_tsv",
+ "type": "tsvector",
+ "primaryKey": false,
+ "notNull": false,
+ "generated": {
+ "as": "to_tsvector('english', \"embedding\".\"content\")",
+ "type": "stored"
+ }
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "emb_kb_id_idx": {
+ "name": "emb_kb_id_idx",
+ "columns": [
+ {
+ "expression": "knowledge_base_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "emb_doc_id_idx": {
+ "name": "emb_doc_id_idx",
+ "columns": [
+ {
+ "expression": "document_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "emb_doc_chunk_idx": {
+ "name": "emb_doc_chunk_idx",
+ "columns": [
+ {
+ "expression": "document_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "chunk_index",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "emb_kb_model_idx": {
+ "name": "emb_kb_model_idx",
+ "columns": [
+ {
+ "expression": "knowledge_base_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "embedding_model",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "emb_kb_enabled_idx": {
+ "name": "emb_kb_enabled_idx",
+ "columns": [
+ {
+ "expression": "knowledge_base_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "enabled",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "emb_doc_enabled_idx": {
+ "name": "emb_doc_enabled_idx",
+ "columns": [
+ {
+ "expression": "document_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "enabled",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "embedding_vector_hnsw_idx": {
+ "name": "embedding_vector_hnsw_idx",
+ "columns": [
+ {
+ "expression": "embedding",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last",
+ "opclass": "vector_cosine_ops"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "hnsw",
+ "with": {
+ "m": 16,
+ "ef_construction": 64
+ }
+ },
+ "emb_tag1_idx": {
+ "name": "emb_tag1_idx",
+ "columns": [
+ {
+ "expression": "tag1",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "emb_tag2_idx": {
+ "name": "emb_tag2_idx",
+ "columns": [
+ {
+ "expression": "tag2",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "emb_tag3_idx": {
+ "name": "emb_tag3_idx",
+ "columns": [
+ {
+ "expression": "tag3",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "emb_tag4_idx": {
+ "name": "emb_tag4_idx",
+ "columns": [
+ {
+ "expression": "tag4",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "emb_tag5_idx": {
+ "name": "emb_tag5_idx",
+ "columns": [
+ {
+ "expression": "tag5",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "emb_tag6_idx": {
+ "name": "emb_tag6_idx",
+ "columns": [
+ {
+ "expression": "tag6",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "emb_tag7_idx": {
+ "name": "emb_tag7_idx",
+ "columns": [
+ {
+ "expression": "tag7",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "emb_number1_idx": {
+ "name": "emb_number1_idx",
+ "columns": [
+ {
+ "expression": "number1",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "emb_number2_idx": {
+ "name": "emb_number2_idx",
+ "columns": [
+ {
+ "expression": "number2",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "emb_number3_idx": {
+ "name": "emb_number3_idx",
+ "columns": [
+ {
+ "expression": "number3",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "emb_number4_idx": {
+ "name": "emb_number4_idx",
+ "columns": [
+ {
+ "expression": "number4",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "emb_number5_idx": {
+ "name": "emb_number5_idx",
+ "columns": [
+ {
+ "expression": "number5",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "emb_date1_idx": {
+ "name": "emb_date1_idx",
+ "columns": [
+ {
+ "expression": "date1",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "emb_date2_idx": {
+ "name": "emb_date2_idx",
+ "columns": [
+ {
+ "expression": "date2",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "emb_boolean1_idx": {
+ "name": "emb_boolean1_idx",
+ "columns": [
+ {
+ "expression": "boolean1",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "emb_boolean2_idx": {
+ "name": "emb_boolean2_idx",
+ "columns": [
+ {
+ "expression": "boolean2",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "emb_boolean3_idx": {
+ "name": "emb_boolean3_idx",
+ "columns": [
+ {
+ "expression": "boolean3",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "emb_content_fts_idx": {
+ "name": "emb_content_fts_idx",
+ "columns": [
+ {
+ "expression": "content_tsv",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "gin",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "embedding_knowledge_base_id_knowledge_base_id_fk": {
+ "name": "embedding_knowledge_base_id_knowledge_base_id_fk",
+ "tableFrom": "embedding",
+ "tableTo": "knowledge_base",
+ "columnsFrom": ["knowledge_base_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "embedding_document_id_document_id_fk": {
+ "name": "embedding_document_id_document_id_fk",
+ "tableFrom": "embedding",
+ "tableTo": "document",
+ "columnsFrom": ["document_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "embedding_not_null_check": {
+ "name": "embedding_not_null_check",
+ "value": "\"embedding\" IS NOT NULL"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.environment": {
+ "name": "environment",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "variables": {
+ "name": "variables",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "environment_user_id_user_id_fk": {
+ "name": "environment_user_id_user_id_fk",
+ "tableFrom": "environment",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "environment_user_id_unique": {
+ "name": "environment_user_id_unique",
+ "nullsNotDistinct": false,
+ "columns": ["user_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.form": {
+ "name": "form",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "identifier": {
+ "name": "identifier",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "is_active": {
+ "name": "is_active",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "customizations": {
+ "name": "customizations",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'{}'"
+ },
+ "auth_type": {
+ "name": "auth_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'public'"
+ },
+ "password": {
+ "name": "password",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "allowed_emails": {
+ "name": "allowed_emails",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'[]'"
+ },
+ "show_branding": {
+ "name": "show_branding",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "form_identifier_idx": {
+ "name": "form_identifier_idx",
+ "columns": [
+ {
+ "expression": "identifier",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "form_workflow_id_idx": {
+ "name": "form_workflow_id_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "form_user_id_idx": {
+ "name": "form_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "form_workflow_id_workflow_id_fk": {
+ "name": "form_workflow_id_workflow_id_fk",
+ "tableFrom": "form",
+ "tableTo": "workflow",
+ "columnsFrom": ["workflow_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "form_user_id_user_id_fk": {
+ "name": "form_user_id_user_id_fk",
+ "tableFrom": "form",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.idempotency_key": {
+ "name": "idempotency_key",
+ "schema": "",
+ "columns": {
+ "key": {
+ "name": "key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "namespace": {
+ "name": "namespace",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'default'"
+ },
+ "result": {
+ "name": "result",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "idempotency_key_namespace_unique": {
+ "name": "idempotency_key_namespace_unique",
+ "columns": [
+ {
+ "expression": "key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "namespace",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idempotency_key_created_at_idx": {
+ "name": "idempotency_key_created_at_idx",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idempotency_key_namespace_idx": {
+ "name": "idempotency_key_namespace_idx",
+ "columns": [
+ {
+ "expression": "namespace",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.invitation": {
+ "name": "invitation",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "inviter_id": {
+ "name": "inviter_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "role": {
+ "name": "role",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "invitation_email_idx": {
+ "name": "invitation_email_idx",
+ "columns": [
+ {
+ "expression": "email",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "invitation_organization_id_idx": {
+ "name": "invitation_organization_id_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "invitation_inviter_id_user_id_fk": {
+ "name": "invitation_inviter_id_user_id_fk",
+ "tableFrom": "invitation",
+ "tableTo": "user",
+ "columnsFrom": ["inviter_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "invitation_organization_id_organization_id_fk": {
+ "name": "invitation_organization_id_organization_id_fk",
+ "tableFrom": "invitation",
+ "tableTo": "organization",
+ "columnsFrom": ["organization_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.knowledge_base": {
+ "name": "knowledge_base",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "token_count": {
+ "name": "token_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "embedding_model": {
+ "name": "embedding_model",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'text-embedding-3-small'"
+ },
+ "embedding_dimension": {
+ "name": "embedding_dimension",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 1536
+ },
+ "chunking_config": {
+ "name": "chunking_config",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{\"maxSize\": 1024, \"minSize\": 1, \"overlap\": 200}'"
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "kb_user_id_idx": {
+ "name": "kb_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "kb_workspace_id_idx": {
+ "name": "kb_workspace_id_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "kb_user_workspace_idx": {
+ "name": "kb_user_workspace_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "kb_deleted_at_idx": {
+ "name": "kb_deleted_at_idx",
+ "columns": [
+ {
+ "expression": "deleted_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "knowledge_base_user_id_user_id_fk": {
+ "name": "knowledge_base_user_id_user_id_fk",
+ "tableFrom": "knowledge_base",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "knowledge_base_workspace_id_workspace_id_fk": {
+ "name": "knowledge_base_workspace_id_workspace_id_fk",
+ "tableFrom": "knowledge_base",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.knowledge_base_tag_definitions": {
+ "name": "knowledge_base_tag_definitions",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "knowledge_base_id": {
+ "name": "knowledge_base_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "tag_slot": {
+ "name": "tag_slot",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "display_name": {
+ "name": "display_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "field_type": {
+ "name": "field_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'text'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "kb_tag_definitions_kb_slot_idx": {
+ "name": "kb_tag_definitions_kb_slot_idx",
+ "columns": [
+ {
+ "expression": "knowledge_base_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "tag_slot",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "kb_tag_definitions_kb_display_name_idx": {
+ "name": "kb_tag_definitions_kb_display_name_idx",
+ "columns": [
+ {
+ "expression": "knowledge_base_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "display_name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "kb_tag_definitions_kb_id_idx": {
+ "name": "kb_tag_definitions_kb_id_idx",
+ "columns": [
+ {
+ "expression": "knowledge_base_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk": {
+ "name": "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk",
+ "tableFrom": "knowledge_base_tag_definitions",
+ "tableTo": "knowledge_base",
+ "columnsFrom": ["knowledge_base_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.mcp_servers": {
+ "name": "mcp_servers",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "transport": {
+ "name": "transport",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "url": {
+ "name": "url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "headers": {
+ "name": "headers",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'{}'"
+ },
+ "timeout": {
+ "name": "timeout",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "default": 30000
+ },
+ "retries": {
+ "name": "retries",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "default": 3
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "last_connected": {
+ "name": "last_connected",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "connection_status": {
+ "name": "connection_status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'disconnected'"
+ },
+ "last_error": {
+ "name": "last_error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status_config": {
+ "name": "status_config",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'{}'"
+ },
+ "tool_count": {
+ "name": "tool_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "default": 0
+ },
+ "last_tools_refresh": {
+ "name": "last_tools_refresh",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "total_requests": {
+ "name": "total_requests",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "default": 0
+ },
+ "last_used": {
+ "name": "last_used",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "mcp_servers_workspace_enabled_idx": {
+ "name": "mcp_servers_workspace_enabled_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "enabled",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "mcp_servers_workspace_deleted_idx": {
+ "name": "mcp_servers_workspace_deleted_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "deleted_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "mcp_servers_workspace_id_workspace_id_fk": {
+ "name": "mcp_servers_workspace_id_workspace_id_fk",
+ "tableFrom": "mcp_servers",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "mcp_servers_created_by_user_id_fk": {
+ "name": "mcp_servers_created_by_user_id_fk",
+ "tableFrom": "mcp_servers",
+ "tableTo": "user",
+ "columnsFrom": ["created_by"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.member": {
+ "name": "member",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "role": {
+ "name": "role",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "member_user_id_unique": {
+ "name": "member_user_id_unique",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "member_organization_id_idx": {
+ "name": "member_organization_id_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "member_user_id_user_id_fk": {
+ "name": "member_user_id_user_id_fk",
+ "tableFrom": "member",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "member_organization_id_organization_id_fk": {
+ "name": "member_organization_id_organization_id_fk",
+ "tableFrom": "member",
+ "tableTo": "organization",
+ "columnsFrom": ["organization_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.memory": {
+ "name": "memory",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "key": {
+ "name": "key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "data": {
+ "name": "data",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "memory_key_idx": {
+ "name": "memory_key_idx",
+ "columns": [
+ {
+ "expression": "key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "memory_workspace_idx": {
+ "name": "memory_workspace_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "memory_workspace_key_idx": {
+ "name": "memory_workspace_key_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "memory_workspace_id_workspace_id_fk": {
+ "name": "memory_workspace_id_workspace_id_fk",
+ "tableFrom": "memory",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.organization": {
+ "name": "organization",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "slug": {
+ "name": "slug",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "logo": {
+ "name": "logo",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "org_usage_limit": {
+ "name": "org_usage_limit",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "storage_used_bytes": {
+ "name": "storage_used_bytes",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "departed_member_usage": {
+ "name": "departed_member_usage",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'0'"
+ },
+ "credit_balance": {
+ "name": "credit_balance",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'0'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.paused_executions": {
+ "name": "paused_executions",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "execution_id": {
+ "name": "execution_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "execution_snapshot": {
+ "name": "execution_snapshot",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "pause_points": {
+ "name": "pause_points",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "total_pause_count": {
+ "name": "total_pause_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "resumed_count": {
+ "name": "resumed_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'paused'"
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'::jsonb"
+ },
+ "paused_at": {
+ "name": "paused_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "paused_executions_workflow_id_idx": {
+ "name": "paused_executions_workflow_id_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "paused_executions_status_idx": {
+ "name": "paused_executions_status_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "paused_executions_execution_id_unique": {
+ "name": "paused_executions_execution_id_unique",
+ "columns": [
+ {
+ "expression": "execution_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "paused_executions_workflow_id_workflow_id_fk": {
+ "name": "paused_executions_workflow_id_workflow_id_fk",
+ "tableFrom": "paused_executions",
+ "tableTo": "workflow",
+ "columnsFrom": ["workflow_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.permission_group": {
+ "name": "permission_group",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "config": {
+ "name": "config",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'"
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "permission_group_organization_id_idx": {
+ "name": "permission_group_organization_id_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "permission_group_created_by_idx": {
+ "name": "permission_group_created_by_idx",
+ "columns": [
+ {
+ "expression": "created_by",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "permission_group_org_name_unique": {
+ "name": "permission_group_org_name_unique",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "permission_group_organization_id_organization_id_fk": {
+ "name": "permission_group_organization_id_organization_id_fk",
+ "tableFrom": "permission_group",
+ "tableTo": "organization",
+ "columnsFrom": ["organization_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "permission_group_created_by_user_id_fk": {
+ "name": "permission_group_created_by_user_id_fk",
+ "tableFrom": "permission_group",
+ "tableTo": "user",
+ "columnsFrom": ["created_by"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.permission_group_member": {
+ "name": "permission_group_member",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "permission_group_id": {
+ "name": "permission_group_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "assigned_by": {
+ "name": "assigned_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "assigned_at": {
+ "name": "assigned_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "permission_group_member_group_id_idx": {
+ "name": "permission_group_member_group_id_idx",
+ "columns": [
+ {
+ "expression": "permission_group_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "permission_group_member_user_id_unique": {
+ "name": "permission_group_member_user_id_unique",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "permission_group_member_permission_group_id_permission_group_id_fk": {
+ "name": "permission_group_member_permission_group_id_permission_group_id_fk",
+ "tableFrom": "permission_group_member",
+ "tableTo": "permission_group",
+ "columnsFrom": ["permission_group_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "permission_group_member_user_id_user_id_fk": {
+ "name": "permission_group_member_user_id_user_id_fk",
+ "tableFrom": "permission_group_member",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "permission_group_member_assigned_by_user_id_fk": {
+ "name": "permission_group_member_assigned_by_user_id_fk",
+ "tableFrom": "permission_group_member",
+ "tableTo": "user",
+ "columnsFrom": ["assigned_by"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.permissions": {
+ "name": "permissions",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "entity_type": {
+ "name": "entity_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "entity_id": {
+ "name": "entity_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "permission_type": {
+ "name": "permission_type",
+ "type": "permission_type",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "permissions_user_id_idx": {
+ "name": "permissions_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "permissions_entity_idx": {
+ "name": "permissions_entity_idx",
+ "columns": [
+ {
+ "expression": "entity_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "entity_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "permissions_user_entity_type_idx": {
+ "name": "permissions_user_entity_type_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "entity_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "permissions_user_entity_permission_idx": {
+ "name": "permissions_user_entity_permission_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "entity_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "permission_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "permissions_user_entity_idx": {
+ "name": "permissions_user_entity_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "entity_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "entity_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "permissions_unique_constraint": {
+ "name": "permissions_unique_constraint",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "entity_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "entity_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "permissions_user_id_user_id_fk": {
+ "name": "permissions_user_id_user_id_fk",
+ "tableFrom": "permissions",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.rate_limit_bucket": {
+ "name": "rate_limit_bucket",
+ "schema": "",
+ "columns": {
+ "key": {
+ "name": "key",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "tokens": {
+ "name": "tokens",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "last_refill_at": {
+ "name": "last_refill_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.resume_queue": {
+ "name": "resume_queue",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "paused_execution_id": {
+ "name": "paused_execution_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "parent_execution_id": {
+ "name": "parent_execution_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "new_execution_id": {
+ "name": "new_execution_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "context_id": {
+ "name": "context_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "resume_input": {
+ "name": "resume_input",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "queued_at": {
+ "name": "queued_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "claimed_at": {
+ "name": "claimed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "completed_at": {
+ "name": "completed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "failure_reason": {
+ "name": "failure_reason",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "resume_queue_parent_status_idx": {
+ "name": "resume_queue_parent_status_idx",
+ "columns": [
+ {
+ "expression": "parent_execution_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "queued_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "resume_queue_new_execution_idx": {
+ "name": "resume_queue_new_execution_idx",
+ "columns": [
+ {
+ "expression": "new_execution_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "resume_queue_paused_execution_id_paused_executions_id_fk": {
+ "name": "resume_queue_paused_execution_id_paused_executions_id_fk",
+ "tableFrom": "resume_queue",
+ "tableTo": "paused_executions",
+ "columnsFrom": ["paused_execution_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.session": {
+ "name": "session",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "token": {
+ "name": "token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "ip_address": {
+ "name": "ip_address",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_agent": {
+ "name": "user_agent",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "active_organization_id": {
+ "name": "active_organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "session_user_id_idx": {
+ "name": "session_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "session_token_idx": {
+ "name": "session_token_idx",
+ "columns": [
+ {
+ "expression": "token",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "session_user_id_user_id_fk": {
+ "name": "session_user_id_user_id_fk",
+ "tableFrom": "session",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "session_active_organization_id_organization_id_fk": {
+ "name": "session_active_organization_id_organization_id_fk",
+ "tableFrom": "session",
+ "tableTo": "organization",
+ "columnsFrom": ["active_organization_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "session_token_unique": {
+ "name": "session_token_unique",
+ "nullsNotDistinct": false,
+ "columns": ["token"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.settings": {
+ "name": "settings",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "theme": {
+ "name": "theme",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'system'"
+ },
+ "auto_connect": {
+ "name": "auto_connect",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "telemetry_enabled": {
+ "name": "telemetry_enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "email_preferences": {
+ "name": "email_preferences",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'"
+ },
+ "billing_usage_notifications_enabled": {
+ "name": "billing_usage_notifications_enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "show_training_controls": {
+ "name": "show_training_controls",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "super_user_mode_enabled": {
+ "name": "super_user_mode_enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "error_notifications_enabled": {
+ "name": "error_notifications_enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "snap_to_grid_size": {
+ "name": "snap_to_grid_size",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "copilot_enabled_models": {
+ "name": "copilot_enabled_models",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'"
+ },
+ "copilot_auto_allowed_tools": {
+ "name": "copilot_auto_allowed_tools",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "settings_user_id_user_id_fk": {
+ "name": "settings_user_id_user_id_fk",
+ "tableFrom": "settings",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "settings_user_id_unique": {
+ "name": "settings_user_id_unique",
+ "nullsNotDistinct": false,
+ "columns": ["user_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.sso_provider": {
+ "name": "sso_provider",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "issuer": {
+ "name": "issuer",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "domain": {
+ "name": "domain",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "oidc_config": {
+ "name": "oidc_config",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "saml_config": {
+ "name": "saml_config",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provider_id": {
+ "name": "provider_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "sso_provider_provider_id_idx": {
+ "name": "sso_provider_provider_id_idx",
+ "columns": [
+ {
+ "expression": "provider_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "sso_provider_domain_idx": {
+ "name": "sso_provider_domain_idx",
+ "columns": [
+ {
+ "expression": "domain",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "sso_provider_user_id_idx": {
+ "name": "sso_provider_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "sso_provider_organization_id_idx": {
+ "name": "sso_provider_organization_id_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "sso_provider_user_id_user_id_fk": {
+ "name": "sso_provider_user_id_user_id_fk",
+ "tableFrom": "sso_provider",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "sso_provider_organization_id_organization_id_fk": {
+ "name": "sso_provider_organization_id_organization_id_fk",
+ "tableFrom": "sso_provider",
+ "tableTo": "organization",
+ "columnsFrom": ["organization_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.subscription": {
+ "name": "subscription",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "plan": {
+ "name": "plan",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "reference_id": {
+ "name": "reference_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "stripe_customer_id": {
+ "name": "stripe_customer_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "stripe_subscription_id": {
+ "name": "stripe_subscription_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "period_start": {
+ "name": "period_start",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "period_end": {
+ "name": "period_end",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "cancel_at_period_end": {
+ "name": "cancel_at_period_end",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "seats": {
+ "name": "seats",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "trial_start": {
+ "name": "trial_start",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "trial_end": {
+ "name": "trial_end",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "subscription_reference_status_idx": {
+ "name": "subscription_reference_status_idx",
+ "columns": [
+ {
+ "expression": "reference_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "check_enterprise_metadata": {
+ "name": "check_enterprise_metadata",
+ "value": "plan != 'enterprise' OR metadata IS NOT NULL"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.template_creators": {
+ "name": "template_creators",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "reference_type": {
+ "name": "reference_type",
+ "type": "template_creator_type",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "reference_id": {
+ "name": "reference_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "profile_image_url": {
+ "name": "profile_image_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "details": {
+ "name": "details",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "verified": {
+ "name": "verified",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "template_creators_reference_idx": {
+ "name": "template_creators_reference_idx",
+ "columns": [
+ {
+ "expression": "reference_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "reference_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "template_creators_reference_id_idx": {
+ "name": "template_creators_reference_id_idx",
+ "columns": [
+ {
+ "expression": "reference_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "template_creators_created_by_idx": {
+ "name": "template_creators_created_by_idx",
+ "columns": [
+ {
+ "expression": "created_by",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "template_creators_created_by_user_id_fk": {
+ "name": "template_creators_created_by_user_id_fk",
+ "tableFrom": "template_creators",
+ "tableTo": "user",
+ "columnsFrom": ["created_by"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.template_stars": {
+ "name": "template_stars",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "template_id": {
+ "name": "template_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "starred_at": {
+ "name": "starred_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "template_stars_user_id_idx": {
+ "name": "template_stars_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "template_stars_template_id_idx": {
+ "name": "template_stars_template_id_idx",
+ "columns": [
+ {
+ "expression": "template_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "template_stars_user_template_idx": {
+ "name": "template_stars_user_template_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "template_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "template_stars_template_user_idx": {
+ "name": "template_stars_template_user_idx",
+ "columns": [
+ {
+ "expression": "template_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "template_stars_starred_at_idx": {
+ "name": "template_stars_starred_at_idx",
+ "columns": [
+ {
+ "expression": "starred_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "template_stars_template_starred_at_idx": {
+ "name": "template_stars_template_starred_at_idx",
+ "columns": [
+ {
+ "expression": "template_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "starred_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "template_stars_user_template_unique": {
+ "name": "template_stars_user_template_unique",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "template_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "template_stars_user_id_user_id_fk": {
+ "name": "template_stars_user_id_user_id_fk",
+ "tableFrom": "template_stars",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "template_stars_template_id_templates_id_fk": {
+ "name": "template_stars_template_id_templates_id_fk",
+ "tableFrom": "template_stars",
+ "tableTo": "templates",
+ "columnsFrom": ["template_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.templates": {
+ "name": "templates",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "details": {
+ "name": "details",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "creator_id": {
+ "name": "creator_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "views": {
+ "name": "views",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "stars": {
+ "name": "stars",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "status": {
+ "name": "status",
+ "type": "template_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "tags": {
+ "name": "tags",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'::text[]"
+ },
+ "required_credentials": {
+ "name": "required_credentials",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'"
+ },
+ "state": {
+ "name": "state",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "og_image_url": {
+ "name": "og_image_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "templates_status_idx": {
+ "name": "templates_status_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "templates_creator_id_idx": {
+ "name": "templates_creator_id_idx",
+ "columns": [
+ {
+ "expression": "creator_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "templates_views_idx": {
+ "name": "templates_views_idx",
+ "columns": [
+ {
+ "expression": "views",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "templates_stars_idx": {
+ "name": "templates_stars_idx",
+ "columns": [
+ {
+ "expression": "stars",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "templates_status_views_idx": {
+ "name": "templates_status_views_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "views",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "templates_status_stars_idx": {
+ "name": "templates_status_stars_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "stars",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "templates_created_at_idx": {
+ "name": "templates_created_at_idx",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "templates_updated_at_idx": {
+ "name": "templates_updated_at_idx",
+ "columns": [
+ {
+ "expression": "updated_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "templates_workflow_id_workflow_id_fk": {
+ "name": "templates_workflow_id_workflow_id_fk",
+ "tableFrom": "templates",
+ "tableTo": "workflow",
+ "columnsFrom": ["workflow_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "templates_creator_id_template_creators_id_fk": {
+ "name": "templates_creator_id_template_creators_id_fk",
+ "tableFrom": "templates",
+ "tableTo": "template_creators",
+ "columnsFrom": ["creator_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.usage_log": {
+ "name": "usage_log",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "category": {
+ "name": "category",
+ "type": "usage_log_category",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "source": {
+ "name": "source",
+ "type": "usage_log_source",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "cost": {
+ "name": "cost",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "execution_id": {
+ "name": "execution_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "usage_log_user_created_at_idx": {
+ "name": "usage_log_user_created_at_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "usage_log_source_idx": {
+ "name": "usage_log_source_idx",
+ "columns": [
+ {
+ "expression": "source",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "usage_log_workspace_id_idx": {
+ "name": "usage_log_workspace_id_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "usage_log_workflow_id_idx": {
+ "name": "usage_log_workflow_id_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "usage_log_user_id_user_id_fk": {
+ "name": "usage_log_user_id_user_id_fk",
+ "tableFrom": "usage_log",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "usage_log_workspace_id_workspace_id_fk": {
+ "name": "usage_log_workspace_id_workspace_id_fk",
+ "tableFrom": "usage_log",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "usage_log_workflow_id_workflow_id_fk": {
+ "name": "usage_log_workflow_id_workflow_id_fk",
+ "tableFrom": "usage_log",
+ "tableTo": "workflow",
+ "columnsFrom": ["workflow_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.user": {
+ "name": "user",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email_verified": {
+ "name": "email_verified",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "image": {
+ "name": "image",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "stripe_customer_id": {
+ "name": "stripe_customer_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "is_super_user": {
+ "name": "is_super_user",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "user_email_unique": {
+ "name": "user_email_unique",
+ "nullsNotDistinct": false,
+ "columns": ["email"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.user_stats": {
+ "name": "user_stats",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "total_manual_executions": {
+ "name": "total_manual_executions",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "total_api_calls": {
+ "name": "total_api_calls",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "total_webhook_triggers": {
+ "name": "total_webhook_triggers",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "total_scheduled_executions": {
+ "name": "total_scheduled_executions",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "total_chat_executions": {
+ "name": "total_chat_executions",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "total_tokens_used": {
+ "name": "total_tokens_used",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "total_cost": {
+ "name": "total_cost",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'0'"
+ },
+ "current_usage_limit": {
+ "name": "current_usage_limit",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'20'"
+ },
+ "usage_limit_updated_at": {
+ "name": "usage_limit_updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "now()"
+ },
+ "current_period_cost": {
+ "name": "current_period_cost",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'0'"
+ },
+ "last_period_cost": {
+ "name": "last_period_cost",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'0'"
+ },
+ "billed_overage_this_period": {
+ "name": "billed_overage_this_period",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'0'"
+ },
+ "pro_period_cost_snapshot": {
+ "name": "pro_period_cost_snapshot",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'0'"
+ },
+ "credit_balance": {
+ "name": "credit_balance",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'0'"
+ },
+ "total_copilot_cost": {
+ "name": "total_copilot_cost",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'0'"
+ },
+ "current_period_copilot_cost": {
+ "name": "current_period_copilot_cost",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'0'"
+ },
+ "last_period_copilot_cost": {
+ "name": "last_period_copilot_cost",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'0'"
+ },
+ "total_copilot_tokens": {
+ "name": "total_copilot_tokens",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "total_copilot_calls": {
+ "name": "total_copilot_calls",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "storage_used_bytes": {
+ "name": "storage_used_bytes",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "last_active": {
+ "name": "last_active",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "billing_blocked": {
+ "name": "billing_blocked",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "billing_blocked_reason": {
+ "name": "billing_blocked_reason",
+ "type": "billing_blocked_reason",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "user_stats_user_id_user_id_fk": {
+ "name": "user_stats_user_id_user_id_fk",
+ "tableFrom": "user_stats",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "user_stats_user_id_unique": {
+ "name": "user_stats_user_id_unique",
+ "nullsNotDistinct": false,
+ "columns": ["user_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.verification": {
+ "name": "verification",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "identifier": {
+ "name": "identifier",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "value": {
+ "name": "value",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "verification_identifier_idx": {
+ "name": "verification_identifier_idx",
+ "columns": [
+ {
+ "expression": "identifier",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "verification_expires_at_idx": {
+ "name": "verification_expires_at_idx",
+ "columns": [
+ {
+ "expression": "expires_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.waitlist": {
+ "name": "waitlist",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "waitlist_email_unique": {
+ "name": "waitlist_email_unique",
+ "nullsNotDistinct": false,
+ "columns": ["email"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.webhook": {
+ "name": "webhook",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "block_id": {
+ "name": "block_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "path": {
+ "name": "path",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provider": {
+ "name": "provider",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "provider_config": {
+ "name": "provider_config",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "is_active": {
+ "name": "is_active",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "failed_count": {
+ "name": "failed_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "default": 0
+ },
+ "last_failed_at": {
+ "name": "last_failed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "credential_set_id": {
+ "name": "credential_set_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "path_idx": {
+ "name": "path_idx",
+ "columns": [
+ {
+ "expression": "path",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_webhook_on_workflow_id_block_id": {
+ "name": "idx_webhook_on_workflow_id_block_id",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "block_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "webhook_credential_set_id_idx": {
+ "name": "webhook_credential_set_id_idx",
+ "columns": [
+ {
+ "expression": "credential_set_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "webhook_workflow_id_workflow_id_fk": {
+ "name": "webhook_workflow_id_workflow_id_fk",
+ "tableFrom": "webhook",
+ "tableTo": "workflow",
+ "columnsFrom": ["workflow_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "webhook_block_id_workflow_blocks_id_fk": {
+ "name": "webhook_block_id_workflow_blocks_id_fk",
+ "tableFrom": "webhook",
+ "tableTo": "workflow_blocks",
+ "columnsFrom": ["block_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "webhook_credential_set_id_credential_set_id_fk": {
+ "name": "webhook_credential_set_id_credential_set_id_fk",
+ "tableFrom": "webhook",
+ "tableTo": "credential_set",
+ "columnsFrom": ["credential_set_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workflow": {
+ "name": "workflow",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "folder_id": {
+ "name": "folder_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "color": {
+ "name": "color",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'#3972F6'"
+ },
+ "last_synced": {
+ "name": "last_synced",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "is_deployed": {
+ "name": "is_deployed",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "deployed_at": {
+ "name": "deployed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "run_count": {
+ "name": "run_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "last_run_at": {
+ "name": "last_run_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "variables": {
+ "name": "variables",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'{}'"
+ }
+ },
+ "indexes": {
+ "workflow_user_id_idx": {
+ "name": "workflow_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_workspace_id_idx": {
+ "name": "workflow_workspace_id_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_user_workspace_idx": {
+ "name": "workflow_user_workspace_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "workflow_user_id_user_id_fk": {
+ "name": "workflow_user_id_user_id_fk",
+ "tableFrom": "workflow",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workflow_workspace_id_workspace_id_fk": {
+ "name": "workflow_workspace_id_workspace_id_fk",
+ "tableFrom": "workflow",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workflow_folder_id_workflow_folder_id_fk": {
+ "name": "workflow_folder_id_workflow_folder_id_fk",
+ "tableFrom": "workflow",
+ "tableTo": "workflow_folder",
+ "columnsFrom": ["folder_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workflow_blocks": {
+ "name": "workflow_blocks",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "position_x": {
+ "name": "position_x",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "position_y": {
+ "name": "position_y",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "horizontal_handles": {
+ "name": "horizontal_handles",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "is_wide": {
+ "name": "is_wide",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "advanced_mode": {
+ "name": "advanced_mode",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "trigger_mode": {
+ "name": "trigger_mode",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "height": {
+ "name": "height",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'0'"
+ },
+ "sub_blocks": {
+ "name": "sub_blocks",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'"
+ },
+ "outputs": {
+ "name": "outputs",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'"
+ },
+ "data": {
+ "name": "data",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'{}'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "workflow_blocks_workflow_id_idx": {
+ "name": "workflow_blocks_workflow_id_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_blocks_type_idx": {
+ "name": "workflow_blocks_type_idx",
+ "columns": [
+ {
+ "expression": "type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "workflow_blocks_workflow_id_workflow_id_fk": {
+ "name": "workflow_blocks_workflow_id_workflow_id_fk",
+ "tableFrom": "workflow_blocks",
+ "tableTo": "workflow",
+ "columnsFrom": ["workflow_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workflow_checkpoints": {
+ "name": "workflow_checkpoints",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "chat_id": {
+ "name": "chat_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "message_id": {
+ "name": "message_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "workflow_state": {
+ "name": "workflow_state",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "workflow_checkpoints_user_id_idx": {
+ "name": "workflow_checkpoints_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_checkpoints_workflow_id_idx": {
+ "name": "workflow_checkpoints_workflow_id_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_checkpoints_chat_id_idx": {
+ "name": "workflow_checkpoints_chat_id_idx",
+ "columns": [
+ {
+ "expression": "chat_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_checkpoints_message_id_idx": {
+ "name": "workflow_checkpoints_message_id_idx",
+ "columns": [
+ {
+ "expression": "message_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_checkpoints_user_workflow_idx": {
+ "name": "workflow_checkpoints_user_workflow_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_checkpoints_workflow_chat_idx": {
+ "name": "workflow_checkpoints_workflow_chat_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "chat_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_checkpoints_created_at_idx": {
+ "name": "workflow_checkpoints_created_at_idx",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_checkpoints_chat_created_at_idx": {
+ "name": "workflow_checkpoints_chat_created_at_idx",
+ "columns": [
+ {
+ "expression": "chat_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "workflow_checkpoints_user_id_user_id_fk": {
+ "name": "workflow_checkpoints_user_id_user_id_fk",
+ "tableFrom": "workflow_checkpoints",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workflow_checkpoints_workflow_id_workflow_id_fk": {
+ "name": "workflow_checkpoints_workflow_id_workflow_id_fk",
+ "tableFrom": "workflow_checkpoints",
+ "tableTo": "workflow",
+ "columnsFrom": ["workflow_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workflow_checkpoints_chat_id_copilot_chats_id_fk": {
+ "name": "workflow_checkpoints_chat_id_copilot_chats_id_fk",
+ "tableFrom": "workflow_checkpoints",
+ "tableTo": "copilot_chats",
+ "columnsFrom": ["chat_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workflow_deployment_version": {
+ "name": "workflow_deployment_version",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "version": {
+ "name": "version",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "state": {
+ "name": "state",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "is_active": {
+ "name": "is_active",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "workflow_deployment_version_workflow_version_unique": {
+ "name": "workflow_deployment_version_workflow_version_unique",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "version",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_deployment_version_workflow_active_idx": {
+ "name": "workflow_deployment_version_workflow_active_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "is_active",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_deployment_version_created_at_idx": {
+ "name": "workflow_deployment_version_created_at_idx",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "workflow_deployment_version_workflow_id_workflow_id_fk": {
+ "name": "workflow_deployment_version_workflow_id_workflow_id_fk",
+ "tableFrom": "workflow_deployment_version",
+ "tableTo": "workflow",
+ "columnsFrom": ["workflow_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workflow_edges": {
+ "name": "workflow_edges",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "source_block_id": {
+ "name": "source_block_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "target_block_id": {
+ "name": "target_block_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "source_handle": {
+ "name": "source_handle",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "target_handle": {
+ "name": "target_handle",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "workflow_edges_workflow_id_idx": {
+ "name": "workflow_edges_workflow_id_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_edges_workflow_source_idx": {
+ "name": "workflow_edges_workflow_source_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "source_block_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_edges_workflow_target_idx": {
+ "name": "workflow_edges_workflow_target_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "target_block_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "workflow_edges_workflow_id_workflow_id_fk": {
+ "name": "workflow_edges_workflow_id_workflow_id_fk",
+ "tableFrom": "workflow_edges",
+ "tableTo": "workflow",
+ "columnsFrom": ["workflow_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workflow_edges_source_block_id_workflow_blocks_id_fk": {
+ "name": "workflow_edges_source_block_id_workflow_blocks_id_fk",
+ "tableFrom": "workflow_edges",
+ "tableTo": "workflow_blocks",
+ "columnsFrom": ["source_block_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workflow_edges_target_block_id_workflow_blocks_id_fk": {
+ "name": "workflow_edges_target_block_id_workflow_blocks_id_fk",
+ "tableFrom": "workflow_edges",
+ "tableTo": "workflow_blocks",
+ "columnsFrom": ["target_block_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workflow_execution_logs": {
+ "name": "workflow_execution_logs",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "execution_id": {
+ "name": "execution_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "state_snapshot_id": {
+ "name": "state_snapshot_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "deployment_version_id": {
+ "name": "deployment_version_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "level": {
+ "name": "level",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'running'"
+ },
+ "trigger": {
+ "name": "trigger",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "started_at": {
+ "name": "started_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "ended_at": {
+ "name": "ended_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "total_duration_ms": {
+ "name": "total_duration_ms",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "execution_data": {
+ "name": "execution_data",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'"
+ },
+ "cost": {
+ "name": "cost",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "files": {
+ "name": "files",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "workflow_execution_logs_workflow_id_idx": {
+ "name": "workflow_execution_logs_workflow_id_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_execution_logs_state_snapshot_id_idx": {
+ "name": "workflow_execution_logs_state_snapshot_id_idx",
+ "columns": [
+ {
+ "expression": "state_snapshot_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_execution_logs_deployment_version_id_idx": {
+ "name": "workflow_execution_logs_deployment_version_id_idx",
+ "columns": [
+ {
+ "expression": "deployment_version_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_execution_logs_trigger_idx": {
+ "name": "workflow_execution_logs_trigger_idx",
+ "columns": [
+ {
+ "expression": "trigger",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_execution_logs_level_idx": {
+ "name": "workflow_execution_logs_level_idx",
+ "columns": [
+ {
+ "expression": "level",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_execution_logs_started_at_idx": {
+ "name": "workflow_execution_logs_started_at_idx",
+ "columns": [
+ {
+ "expression": "started_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_execution_logs_execution_id_unique": {
+ "name": "workflow_execution_logs_execution_id_unique",
+ "columns": [
+ {
+ "expression": "execution_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_execution_logs_workflow_started_at_idx": {
+ "name": "workflow_execution_logs_workflow_started_at_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "started_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_execution_logs_workspace_started_at_idx": {
+ "name": "workflow_execution_logs_workspace_started_at_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "started_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "workflow_execution_logs_workflow_id_workflow_id_fk": {
+ "name": "workflow_execution_logs_workflow_id_workflow_id_fk",
+ "tableFrom": "workflow_execution_logs",
+ "tableTo": "workflow",
+ "columnsFrom": ["workflow_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workflow_execution_logs_workspace_id_workspace_id_fk": {
+ "name": "workflow_execution_logs_workspace_id_workspace_id_fk",
+ "tableFrom": "workflow_execution_logs",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workflow_execution_logs_state_snapshot_id_workflow_execution_snapshots_id_fk": {
+ "name": "workflow_execution_logs_state_snapshot_id_workflow_execution_snapshots_id_fk",
+ "tableFrom": "workflow_execution_logs",
+ "tableTo": "workflow_execution_snapshots",
+ "columnsFrom": ["state_snapshot_id"],
+ "columnsTo": ["id"],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "workflow_execution_logs_deployment_version_id_workflow_deployment_version_id_fk": {
+ "name": "workflow_execution_logs_deployment_version_id_workflow_deployment_version_id_fk",
+ "tableFrom": "workflow_execution_logs",
+ "tableTo": "workflow_deployment_version",
+ "columnsFrom": ["deployment_version_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workflow_execution_snapshots": {
+ "name": "workflow_execution_snapshots",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "state_hash": {
+ "name": "state_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "state_data": {
+ "name": "state_data",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "workflow_snapshots_workflow_id_idx": {
+ "name": "workflow_snapshots_workflow_id_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_snapshots_hash_idx": {
+ "name": "workflow_snapshots_hash_idx",
+ "columns": [
+ {
+ "expression": "state_hash",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_snapshots_workflow_hash_idx": {
+ "name": "workflow_snapshots_workflow_hash_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "state_hash",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_snapshots_created_at_idx": {
+ "name": "workflow_snapshots_created_at_idx",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "workflow_execution_snapshots_workflow_id_workflow_id_fk": {
+ "name": "workflow_execution_snapshots_workflow_id_workflow_id_fk",
+ "tableFrom": "workflow_execution_snapshots",
+ "tableTo": "workflow",
+ "columnsFrom": ["workflow_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workflow_folder": {
+ "name": "workflow_folder",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "parent_id": {
+ "name": "parent_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "color": {
+ "name": "color",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'#6B7280'"
+ },
+ "is_expanded": {
+ "name": "is_expanded",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "sort_order": {
+ "name": "sort_order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "workflow_folder_user_idx": {
+ "name": "workflow_folder_user_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_folder_workspace_parent_idx": {
+ "name": "workflow_folder_workspace_parent_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "parent_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_folder_parent_sort_idx": {
+ "name": "workflow_folder_parent_sort_idx",
+ "columns": [
+ {
+ "expression": "parent_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "sort_order",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "workflow_folder_user_id_user_id_fk": {
+ "name": "workflow_folder_user_id_user_id_fk",
+ "tableFrom": "workflow_folder",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workflow_folder_workspace_id_workspace_id_fk": {
+ "name": "workflow_folder_workspace_id_workspace_id_fk",
+ "tableFrom": "workflow_folder",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workflow_mcp_server": {
+ "name": "workflow_mcp_server",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "workflow_mcp_server_workspace_id_idx": {
+ "name": "workflow_mcp_server_workspace_id_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_mcp_server_created_by_idx": {
+ "name": "workflow_mcp_server_created_by_idx",
+ "columns": [
+ {
+ "expression": "created_by",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "workflow_mcp_server_workspace_id_workspace_id_fk": {
+ "name": "workflow_mcp_server_workspace_id_workspace_id_fk",
+ "tableFrom": "workflow_mcp_server",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workflow_mcp_server_created_by_user_id_fk": {
+ "name": "workflow_mcp_server_created_by_user_id_fk",
+ "tableFrom": "workflow_mcp_server",
+ "tableTo": "user",
+ "columnsFrom": ["created_by"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workflow_mcp_tool": {
+ "name": "workflow_mcp_tool",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "server_id": {
+ "name": "server_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "tool_name": {
+ "name": "tool_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "tool_description": {
+ "name": "tool_description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "parameter_schema": {
+ "name": "parameter_schema",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "workflow_mcp_tool_server_id_idx": {
+ "name": "workflow_mcp_tool_server_id_idx",
+ "columns": [
+ {
+ "expression": "server_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_mcp_tool_workflow_id_idx": {
+ "name": "workflow_mcp_tool_workflow_id_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_mcp_tool_server_workflow_unique": {
+ "name": "workflow_mcp_tool_server_workflow_unique",
+ "columns": [
+ {
+ "expression": "server_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "workflow_mcp_tool_server_id_workflow_mcp_server_id_fk": {
+ "name": "workflow_mcp_tool_server_id_workflow_mcp_server_id_fk",
+ "tableFrom": "workflow_mcp_tool",
+ "tableTo": "workflow_mcp_server",
+ "columnsFrom": ["server_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workflow_mcp_tool_workflow_id_workflow_id_fk": {
+ "name": "workflow_mcp_tool_workflow_id_workflow_id_fk",
+ "tableFrom": "workflow_mcp_tool",
+ "tableTo": "workflow",
+ "columnsFrom": ["workflow_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workflow_schedule": {
+ "name": "workflow_schedule",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "block_id": {
+ "name": "block_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "cron_expression": {
+ "name": "cron_expression",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "next_run_at": {
+ "name": "next_run_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_ran_at": {
+ "name": "last_ran_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_queued_at": {
+ "name": "last_queued_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "trigger_type": {
+ "name": "trigger_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "timezone": {
+ "name": "timezone",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'UTC'"
+ },
+ "failed_count": {
+ "name": "failed_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'active'"
+ },
+ "last_failed_at": {
+ "name": "last_failed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "workflow_schedule_workflow_block_unique": {
+ "name": "workflow_schedule_workflow_block_unique",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "block_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "workflow_schedule_workflow_id_workflow_id_fk": {
+ "name": "workflow_schedule_workflow_id_workflow_id_fk",
+ "tableFrom": "workflow_schedule",
+ "tableTo": "workflow",
+ "columnsFrom": ["workflow_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workflow_schedule_block_id_workflow_blocks_id_fk": {
+ "name": "workflow_schedule_block_id_workflow_blocks_id_fk",
+ "tableFrom": "workflow_schedule",
+ "tableTo": "workflow_blocks",
+ "columnsFrom": ["block_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workflow_subflows": {
+ "name": "workflow_subflows",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "config": {
+ "name": "config",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "workflow_subflows_workflow_id_idx": {
+ "name": "workflow_subflows_workflow_id_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_subflows_workflow_type_idx": {
+ "name": "workflow_subflows_workflow_type_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "workflow_subflows_workflow_id_workflow_id_fk": {
+ "name": "workflow_subflows_workflow_id_workflow_id_fk",
+ "tableFrom": "workflow_subflows",
+ "tableTo": "workflow",
+ "columnsFrom": ["workflow_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workspace": {
+ "name": "workspace",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "owner_id": {
+ "name": "owner_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "billed_account_user_id": {
+ "name": "billed_account_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "allow_personal_api_keys": {
+ "name": "allow_personal_api_keys",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "workspace_owner_id_user_id_fk": {
+ "name": "workspace_owner_id_user_id_fk",
+ "tableFrom": "workspace",
+ "tableTo": "user",
+ "columnsFrom": ["owner_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workspace_billed_account_user_id_user_id_fk": {
+ "name": "workspace_billed_account_user_id_user_id_fk",
+ "tableFrom": "workspace",
+ "tableTo": "user",
+ "columnsFrom": ["billed_account_user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workspace_byok_keys": {
+ "name": "workspace_byok_keys",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provider_id": {
+ "name": "provider_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "encrypted_api_key": {
+ "name": "encrypted_api_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "workspace_byok_provider_unique": {
+ "name": "workspace_byok_provider_unique",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "provider_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workspace_byok_workspace_idx": {
+ "name": "workspace_byok_workspace_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "workspace_byok_keys_workspace_id_workspace_id_fk": {
+ "name": "workspace_byok_keys_workspace_id_workspace_id_fk",
+ "tableFrom": "workspace_byok_keys",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workspace_byok_keys_created_by_user_id_fk": {
+ "name": "workspace_byok_keys_created_by_user_id_fk",
+ "tableFrom": "workspace_byok_keys",
+ "tableTo": "user",
+ "columnsFrom": ["created_by"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workspace_environment": {
+ "name": "workspace_environment",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "variables": {
+ "name": "variables",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "workspace_environment_workspace_unique": {
+ "name": "workspace_environment_workspace_unique",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "workspace_environment_workspace_id_workspace_id_fk": {
+ "name": "workspace_environment_workspace_id_workspace_id_fk",
+ "tableFrom": "workspace_environment",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workspace_file": {
+ "name": "workspace_file",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "key": {
+ "name": "key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "size": {
+ "name": "size",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "uploaded_by": {
+ "name": "uploaded_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "uploaded_at": {
+ "name": "uploaded_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "workspace_file_workspace_id_idx": {
+ "name": "workspace_file_workspace_id_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workspace_file_key_idx": {
+ "name": "workspace_file_key_idx",
+ "columns": [
+ {
+ "expression": "key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "workspace_file_workspace_id_workspace_id_fk": {
+ "name": "workspace_file_workspace_id_workspace_id_fk",
+ "tableFrom": "workspace_file",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workspace_file_uploaded_by_user_id_fk": {
+ "name": "workspace_file_uploaded_by_user_id_fk",
+ "tableFrom": "workspace_file",
+ "tableTo": "user",
+ "columnsFrom": ["uploaded_by"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "workspace_file_key_unique": {
+ "name": "workspace_file_key_unique",
+ "nullsNotDistinct": false,
+ "columns": ["key"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workspace_files": {
+ "name": "workspace_files",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "key": {
+ "name": "key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "context": {
+ "name": "context",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "original_name": {
+ "name": "original_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "content_type": {
+ "name": "content_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "size": {
+ "name": "size",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "uploaded_at": {
+ "name": "uploaded_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "workspace_files_key_idx": {
+ "name": "workspace_files_key_idx",
+ "columns": [
+ {
+ "expression": "key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workspace_files_user_id_idx": {
+ "name": "workspace_files_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workspace_files_workspace_id_idx": {
+ "name": "workspace_files_workspace_id_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workspace_files_context_idx": {
+ "name": "workspace_files_context_idx",
+ "columns": [
+ {
+ "expression": "context",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "workspace_files_user_id_user_id_fk": {
+ "name": "workspace_files_user_id_user_id_fk",
+ "tableFrom": "workspace_files",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workspace_files_workspace_id_workspace_id_fk": {
+ "name": "workspace_files_workspace_id_workspace_id_fk",
+ "tableFrom": "workspace_files",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "workspace_files_key_unique": {
+ "name": "workspace_files_key_unique",
+ "nullsNotDistinct": false,
+ "columns": ["key"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workspace_invitation": {
+ "name": "workspace_invitation",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "inviter_id": {
+ "name": "inviter_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "role": {
+ "name": "role",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'member'"
+ },
+ "status": {
+ "name": "status",
+ "type": "workspace_invitation_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "token": {
+ "name": "token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "permissions": {
+ "name": "permissions",
+ "type": "permission_type",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'admin'"
+ },
+ "org_invitation_id": {
+ "name": "org_invitation_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "workspace_invitation_workspace_id_workspace_id_fk": {
+ "name": "workspace_invitation_workspace_id_workspace_id_fk",
+ "tableFrom": "workspace_invitation",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workspace_invitation_inviter_id_user_id_fk": {
+ "name": "workspace_invitation_inviter_id_user_id_fk",
+ "tableFrom": "workspace_invitation",
+ "tableTo": "user",
+ "columnsFrom": ["inviter_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "workspace_invitation_token_unique": {
+ "name": "workspace_invitation_token_unique",
+ "nullsNotDistinct": false,
+ "columns": ["token"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workspace_notification_delivery": {
+ "name": "workspace_notification_delivery",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "subscription_id": {
+ "name": "subscription_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "execution_id": {
+ "name": "execution_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "notification_delivery_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "attempts": {
+ "name": "attempts",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "last_attempt_at": {
+ "name": "last_attempt_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "next_attempt_at": {
+ "name": "next_attempt_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "response_status": {
+ "name": "response_status",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "response_body": {
+ "name": "response_body",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "error_message": {
+ "name": "error_message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "workspace_notification_delivery_subscription_id_idx": {
+ "name": "workspace_notification_delivery_subscription_id_idx",
+ "columns": [
+ {
+ "expression": "subscription_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workspace_notification_delivery_execution_id_idx": {
+ "name": "workspace_notification_delivery_execution_id_idx",
+ "columns": [
+ {
+ "expression": "execution_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workspace_notification_delivery_status_idx": {
+ "name": "workspace_notification_delivery_status_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workspace_notification_delivery_next_attempt_idx": {
+ "name": "workspace_notification_delivery_next_attempt_idx",
+ "columns": [
+ {
+ "expression": "next_attempt_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "workspace_notification_delivery_subscription_id_workspace_notification_subscription_id_fk": {
+ "name": "workspace_notification_delivery_subscription_id_workspace_notification_subscription_id_fk",
+ "tableFrom": "workspace_notification_delivery",
+ "tableTo": "workspace_notification_subscription",
+ "columnsFrom": ["subscription_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workspace_notification_delivery_workflow_id_workflow_id_fk": {
+ "name": "workspace_notification_delivery_workflow_id_workflow_id_fk",
+ "tableFrom": "workspace_notification_delivery",
+ "tableTo": "workflow",
+ "columnsFrom": ["workflow_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workspace_notification_subscription": {
+ "name": "workspace_notification_subscription",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "notification_type": {
+ "name": "notification_type",
+ "type": "notification_type",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "workflow_ids": {
+ "name": "workflow_ids",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'::text[]"
+ },
+ "all_workflows": {
+ "name": "all_workflows",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "level_filter": {
+ "name": "level_filter",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "ARRAY['info', 'error']::text[]"
+ },
+ "trigger_filter": {
+ "name": "trigger_filter",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "ARRAY['api', 'webhook', 'schedule', 'manual', 'chat']::text[]"
+ },
+ "include_final_output": {
+ "name": "include_final_output",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "include_trace_spans": {
+ "name": "include_trace_spans",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "include_rate_limits": {
+ "name": "include_rate_limits",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "include_usage_data": {
+ "name": "include_usage_data",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "webhook_config": {
+ "name": "webhook_config",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "email_recipients": {
+ "name": "email_recipients",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "slack_config": {
+ "name": "slack_config",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "alert_config": {
+ "name": "alert_config",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_alert_at": {
+ "name": "last_alert_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "active": {
+ "name": "active",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "workspace_notification_workspace_id_idx": {
+ "name": "workspace_notification_workspace_id_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workspace_notification_active_idx": {
+ "name": "workspace_notification_active_idx",
+ "columns": [
+ {
+ "expression": "active",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workspace_notification_type_idx": {
+ "name": "workspace_notification_type_idx",
+ "columns": [
+ {
+ "expression": "notification_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "workspace_notification_subscription_workspace_id_workspace_id_fk": {
+ "name": "workspace_notification_subscription_workspace_id_workspace_id_fk",
+ "tableFrom": "workspace_notification_subscription",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workspace_notification_subscription_created_by_user_id_fk": {
+ "name": "workspace_notification_subscription_created_by_user_id_fk",
+ "tableFrom": "workspace_notification_subscription",
+ "tableTo": "user",
+ "columnsFrom": ["created_by"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ }
+ },
+ "enums": {
+ "public.a2a_task_status": {
+ "name": "a2a_task_status",
+ "schema": "public",
+ "values": [
+ "submitted",
+ "working",
+ "input-required",
+ "completed",
+ "failed",
+ "canceled",
+ "rejected",
+ "auth-required",
+ "unknown"
+ ]
+ },
+ "public.billing_blocked_reason": {
+ "name": "billing_blocked_reason",
+ "schema": "public",
+ "values": ["payment_failed", "dispute"]
+ },
+ "public.credential_set_invitation_status": {
+ "name": "credential_set_invitation_status",
+ "schema": "public",
+ "values": ["pending", "accepted", "expired", "cancelled"]
+ },
+ "public.credential_set_member_status": {
+ "name": "credential_set_member_status",
+ "schema": "public",
+ "values": ["active", "pending", "revoked"]
+ },
+ "public.notification_delivery_status": {
+ "name": "notification_delivery_status",
+ "schema": "public",
+ "values": ["pending", "in_progress", "success", "failed"]
+ },
+ "public.notification_type": {
+ "name": "notification_type",
+ "schema": "public",
+ "values": ["webhook", "email", "slack"]
+ },
+ "public.permission_type": {
+ "name": "permission_type",
+ "schema": "public",
+ "values": ["admin", "write", "read"]
+ },
+ "public.template_creator_type": {
+ "name": "template_creator_type",
+ "schema": "public",
+ "values": ["user", "organization"]
+ },
+ "public.template_status": {
+ "name": "template_status",
+ "schema": "public",
+ "values": ["pending", "approved", "rejected"]
+ },
+ "public.usage_log_category": {
+ "name": "usage_log_category",
+ "schema": "public",
+ "values": ["model", "fixed"]
+ },
+ "public.usage_log_source": {
+ "name": "usage_log_source",
+ "schema": "public",
+ "values": ["workflow", "wand", "copilot"]
+ },
+ "public.workspace_invitation_status": {
+ "name": "workspace_invitation_status",
+ "schema": "public",
+ "values": ["pending", "accepted", "rejected", "cancelled"]
+ }
+ },
+ "schemas": {},
+ "sequences": {},
+ "roles": {},
+ "policies": {},
+ "views": {},
+ "_meta": {
+ "columns": {},
+ "schemas": {},
+ "tables": {}
+ }
+}
diff --git a/packages/db/migrations/meta/_journal.json b/packages/db/migrations/meta/_journal.json
index 0d0bfcc1c0..30639b8a2e 100644
--- a/packages/db/migrations/meta/_journal.json
+++ b/packages/db/migrations/meta/_journal.json
@@ -967,6 +967,13 @@
"when": 1768027253808,
"tag": "0138_faulty_gamma_corps",
"breakpoints": true
+ },
+ {
+ "idx": 139,
+ "version": "7",
+ "when": 1768260112533,
+ "tag": "0139_late_cargill",
+ "breakpoints": true
}
]
}
diff --git a/packages/db/schema.ts b/packages/db/schema.ts
index 11822462ee..b596b01168 100644
--- a/packages/db/schema.ts
+++ b/packages/db/schema.ts
@@ -1785,6 +1785,153 @@ export const workflowMcpTool = pgTable(
})
)
+/**
+ * A2A Task State Enum (v0.2.6)
+ */
+export const a2aTaskStatusEnum = pgEnum('a2a_task_status', [
+ 'submitted',
+ 'working',
+ 'input-required',
+ 'completed',
+ 'failed',
+ 'canceled',
+ 'rejected',
+ 'auth-required',
+ 'unknown',
+])
+
+/**
+ * A2A Agents - Workflows exposed as A2A-compatible agents
+ * These agents can be called by external A2A clients
+ */
+export const a2aAgent = pgTable(
+ 'a2a_agent',
+ {
+ id: text('id').primaryKey(),
+ workspaceId: text('workspace_id')
+ .notNull()
+ .references(() => workspace.id, { onDelete: 'cascade' }),
+ workflowId: text('workflow_id')
+ .notNull()
+ .references(() => workflow.id, { onDelete: 'cascade' }),
+ createdBy: text('created_by')
+ .notNull()
+ .references(() => user.id, { onDelete: 'cascade' }),
+
+ /** Agent name (used in Agent Card) */
+ name: text('name').notNull(),
+ /** Agent description */
+ description: text('description'),
+ /** Agent version */
+ version: text('version').notNull().default('1.0.0'),
+
+ /** Agent capabilities (streaming, pushNotifications, etc.) */
+ capabilities: jsonb('capabilities').notNull().default('{}'),
+ /** Agent skills derived from workflow */
+ skills: jsonb('skills').notNull().default('[]'),
+ /** Authentication configuration */
+ authentication: jsonb('authentication').notNull().default('{}'),
+ /** Agent card signatures for verification (v0.3) */
+ signatures: jsonb('signatures').default('[]'),
+
+ /** Whether the agent is published and discoverable */
+ isPublished: boolean('is_published').notNull().default(false),
+ /** When the agent was published */
+ publishedAt: timestamp('published_at'),
+
+ createdAt: timestamp('created_at').notNull().defaultNow(),
+ updatedAt: timestamp('updated_at').notNull().defaultNow(),
+ },
+ (table) => ({
+ workspaceIdIdx: index('a2a_agent_workspace_id_idx').on(table.workspaceId),
+ workflowIdIdx: index('a2a_agent_workflow_id_idx').on(table.workflowId),
+ createdByIdx: index('a2a_agent_created_by_idx').on(table.createdBy),
+ workspaceWorkflowUnique: uniqueIndex('a2a_agent_workspace_workflow_unique').on(
+ table.workspaceId,
+ table.workflowId
+ ),
+ })
+)
+
+/**
+ * A2A Tasks - Tracks task state for A2A agent interactions (v0.3)
+ * Each task represents a conversation/interaction with an agent
+ */
+export const a2aTask = pgTable(
+ 'a2a_task',
+ {
+ id: text('id').primaryKey(),
+ agentId: text('agent_id')
+ .notNull()
+ .references(() => a2aAgent.id, { onDelete: 'cascade' }),
+
+ /** Context ID for multi-turn conversations (maps to API contextId) */
+ sessionId: text('session_id'),
+
+ /** Task state */
+ status: a2aTaskStatusEnum('status').notNull().default('submitted'),
+
+ /** Message history (maps to API history, array of TaskMessage) */
+ messages: jsonb('messages').notNull().default('[]'),
+
+ /** Structured output artifacts */
+ artifacts: jsonb('artifacts').default('[]'),
+
+ /** Link to workflow execution */
+ executionId: text('execution_id'),
+
+ /** Additional metadata */
+ metadata: jsonb('metadata').default('{}'),
+
+ createdAt: timestamp('created_at').notNull().defaultNow(),
+ updatedAt: timestamp('updated_at').notNull().defaultNow(),
+ completedAt: timestamp('completed_at'),
+ },
+ (table) => ({
+ agentIdIdx: index('a2a_task_agent_id_idx').on(table.agentId),
+ sessionIdIdx: index('a2a_task_session_id_idx').on(table.sessionId),
+ statusIdx: index('a2a_task_status_idx').on(table.status),
+ executionIdIdx: index('a2a_task_execution_id_idx').on(table.executionId),
+ createdAtIdx: index('a2a_task_created_at_idx').on(table.createdAt),
+ })
+)
+
+/**
+ * A2A Push Notification Config - Webhook configuration for task updates
+ * Stores push notification webhooks for async task updates
+ */
+export const a2aPushNotificationConfig = pgTable(
+ 'a2a_push_notification_config',
+ {
+ id: text('id').primaryKey(),
+ taskId: text('task_id')
+ .notNull()
+ .references(() => a2aTask.id, { onDelete: 'cascade' }),
+
+ /** Webhook URL for notifications */
+ url: text('url').notNull(),
+
+ /** Optional token for client-side validation */
+ token: text('token'),
+
+ /** Authentication schemes (e.g., ['bearer', 'apiKey']) */
+ authSchemes: jsonb('auth_schemes').default('[]'),
+
+ /** Authentication credentials hint */
+ authCredentials: text('auth_credentials'),
+
+ /** Whether this config is active */
+ isActive: boolean('is_active').notNull().default(true),
+
+ createdAt: timestamp('created_at').notNull().defaultNow(),
+ updatedAt: timestamp('updated_at').notNull().defaultNow(),
+ },
+ (table) => ({
+ taskIdIdx: index('a2a_push_notification_config_task_id_idx').on(table.taskId),
+ taskIdUnique: uniqueIndex('a2a_push_notification_config_task_unique').on(table.taskId),
+ })
+)
+
export const usageLogCategoryEnum = pgEnum('usage_log_category', ['model', 'fixed'])
export const usageLogSourceEnum = pgEnum('usage_log_source', ['workflow', 'wand', 'copilot'])