diff --git a/apps/docs/components/icons.tsx b/apps/docs/components/icons.tsx index 41732af3b29..cdd2ab94016 100644 --- a/apps/docs/components/icons.tsx +++ b/apps/docs/components/icons.tsx @@ -6823,6 +6823,29 @@ export function SixtyfourIcon(props: SVGProps) { ) } +export function SimTriggerIcon(props: SVGProps) { + return ( + + ) +} + export function SimilarwebIcon(props: SVGProps) { return ( = { mistral_parse_v3: MistralIcon, monday: MondayIcon, mongodb: MongoDBIcon, + mysql: MySQLIcon, neo4j: Neo4jIcon, neverbounce: NeverBounceIcon, new_relic: NewRelicIcon, @@ -373,6 +380,7 @@ export const blockTypeToIconMap: Record = { pinecone: PineconeIcon, pipedrive: PipedriveIcon, polymarket: PolymarketIcon, + postgresql: PostgresIcon, posthog: PosthogIcon, profound: ProfoundIcon, prospeo: ProspeoIcon, @@ -402,13 +410,17 @@ export const blockTypeToIconMap: Record = { serper: SerperIcon, servicenow: ServiceNowIcon, ses: SESIcon, + sftp: SftpIcon, sharepoint: MicrosoftSharepointIcon, sharepoint_v2: MicrosoftSharepointIcon, shopify: ShopifyIcon, + sim_workspace_event: SimTriggerIcon, similarweb: SimilarwebIcon, sixtyfour: SixtyfourIcon, slack: SlackIcon, + smtp: SmtpIcon, sqs: SQSIcon, + ssh: SshIcon, stagehand: StagehandIcon, stripe: StripeIcon, sts: STSIcon, diff --git a/apps/docs/content/docs/en/tools/enrichment.mdx b/apps/docs/content/docs/en/blocks/enrichment.mdx similarity index 100% rename from apps/docs/content/docs/en/tools/enrichment.mdx rename to apps/docs/content/docs/en/blocks/enrichment.mdx diff --git a/apps/docs/content/docs/en/blocks/logs.mdx b/apps/docs/content/docs/en/blocks/logs.mdx new file mode 100644 index 00000000000..b0a9a44b715 --- /dev/null +++ b/apps/docs/content/docs/en/blocks/logs.mdx @@ -0,0 +1,57 @@ +--- +title: Logs +--- + +import { Callout } from 'fumadocs-ui/components/callout' +import { Tab, Tabs } from 'fumadocs-ui/components/tabs' +import { FAQ } from '@/components/ui/faq' + +The Logs block queries workflow run logs in the current workspace and fetches full details for individual runs — the same data you see on the Logs page, available to your workflows. + +## Operations + + + +

Find runs matching a set of filters. Returns only the matching run IDs, ordered newest-first by default.

+
    +
  • Workflows: select specific workflows, or leave empty for all (comma-separated IDs in advanced mode)
  • +
  • Status: info, error, running, pending, cancelled (empty for all)
  • +
  • Time Range: presets from the past 30 minutes to the past 30 days, or explicit ISO start/end dates in advanced mode
  • +
  • Cost: compare run cost against a credit threshold (e.g. ≥ 10 credits)
  • +
  • Duration: compare run duration against a millisecond threshold
  • +
  • Limit: maximum run IDs to return (default 100, max 200)
  • +
+ + +

Fetch everything about a single run by its run ID:

+
    +
  • runId, workflowId, workflowName
  • +
  • status, trigger, startedAt, durationMs
  • +
  • cost: run cost in credits
  • +
  • traceSpans: the full trace — per-block inputs, outputs, timings, and tool calls
  • +
  • finalOutput: the run's final output
  • +
+
+ + +## Typical Pattern + +Query for the runs you care about, then loop over the returned IDs and fetch details for each: + +1. **Query Logs** with `Status: error` and `Time Range: Past 24 hours` → `` +2. Loop over the IDs and call **Get Run Details** → inspect `` to find the failing block +3. Act on it — post a summary to Slack, file a ticket, or feed the trace to an Agent block for diagnosis + +This pairs naturally with the [Sim trigger](/triggers/sim): the trigger hands you the `runId` that fired the event, and Get Run Details gives you the full trace. + + + The block always operates on the current workspace. Costs are denominated in credits, both for + the cost filter and the cost output. + + + diff --git a/apps/docs/content/docs/en/blocks/meta.json b/apps/docs/content/docs/en/blocks/meta.json index 2e1ad2ccad4..5409369e306 100644 --- a/apps/docs/content/docs/en/blocks/meta.json +++ b/apps/docs/content/docs/en/blocks/meta.json @@ -5,11 +5,13 @@ "api", "condition", "credential", + "enrichment", "evaluator", "function", "guardrails", "human-in-the-loop", "knowledge", + "logs", "loop", "parallel", "response", diff --git a/apps/docs/content/docs/en/execution/api.mdx b/apps/docs/content/docs/en/execution/api.mdx index 5e8e2ea07ca..e41d975a618 100644 --- a/apps/docs/content/docs/en/execution/api.mdx +++ b/apps/docs/content/docs/en/execution/api.mdx @@ -6,7 +6,7 @@ import { Callout } from 'fumadocs-ui/components/callout' import { Tab, Tabs } from 'fumadocs-ui/components/tabs' import { Video } from '@/components/ui/video' -Sim provides a comprehensive external API for querying workflow run logs and setting up webhooks for real-time notifications when workflows complete. +Sim provides a comprehensive external API for querying workflow run logs. To react to workflow runs in real time, use the Sim trigger block to run a workflow on workspace events like execution errors, successes, and deployments. ## Authentication @@ -246,230 +246,13 @@ Retrieve run details including the workflow state snapshot. -## Notifications - -Get real-time notifications when workflow runs complete via webhook, email, or Slack. Notifications are configured at the workspace level from the Logs page. - -### Configuration - -Configure notifications from the Logs page by clicking the menu button and selecting "Configure Notifications". - -**Notification Channels:** -- **Webhook**: Send HTTP POST requests to your endpoint -- **Email**: Receive email notifications with run details -- **Slack**: Post messages to a Slack channel - -**Workflow Selection:** -- Select specific workflows to monitor -- Or choose "All Workflows" to include current and future workflows - -**Filtering Options:** -- `levelFilter`: Log levels to receive (`info`, `error`) -- `triggerFilter`: Trigger types to receive (`api`, `webhook`, `schedule`, `manual`, `chat`) - -**Optional Data:** -- `includeFinalOutput`: Include the workflow's final output -- `includeTraceSpans`: Include detailed trace spans -- `includeRateLimits`: Include rate limit information (sync/async limits and remaining) -- `includeUsageData`: Include billing period usage and limits - -### Alert Rules - -Instead of receiving notifications for every run, configure alert rules to be notified only when issues are detected: - -**Consecutive Failures** -- Alert after X consecutive failed runs (e.g., 3 failures in a row) -- Resets when a run succeeds - -**Failure Rate** -- Alert when failure rate exceeds X% over the last Y hours -- Requires minimum 5 runs in the window -- Only triggers after the full time window has elapsed - -**Latency Threshold** -- Alert when any run takes longer than X seconds -- Useful for catching slow or hanging workflows - -**Latency Spike** -- Alert when a run is X% slower than the average -- Compares against the average duration over the configured time window -- Requires minimum 5 runs to establish baseline - -**Cost Threshold** -- Alert when a single run costs more than $X -- Useful for catching expensive LLM calls - -**No Activity** -- Alert when no runs occur within X hours -- Useful for monitoring scheduled workflows that should run regularly - -**Error Count** -- Alert when error count exceeds X within a time window -- Tracks total errors, not consecutive - -All alert types include a 1-hour cooldown to prevent notification spam. - -### Webhook Configuration - -For webhooks, additional options are available: -- `url`: Your webhook endpoint URL -- `secret`: Optional secret for HMAC signature verification - -### Payload Structure - -When a workflow run completes, Sim sends the following payload (via webhook POST, email, or Slack): - -```json -{ - "id": "evt_123", - "type": "workflow.execution.completed", - "timestamp": 1735925767890, - "data": { - "workflowId": "wf_xyz789", - "executionId": "exec_def456", - "status": "success", - "level": "info", - "trigger": "api", - "startedAt": "2025-01-01T12:34:56.789Z", - "endedAt": "2025-01-01T12:34:57.123Z", - "totalDurationMs": 334, - "cost": { - "total": 0.00234, - "tokens": { - "prompt": 123, - "completion": 456, - "total": 579 - }, - "models": { - "gpt-4o": { - "input": 0.001, - "output": 0.00134, - "total": 0.00234, - "tokens": { - "prompt": 123, - "completion": 456, - "total": 579 - } - } - } - }, - "files": null, - "finalOutput": {...}, // Only if includeFinalOutput=true - "traceSpans": [...], // Only if includeTraceSpans=true - "rateLimits": {...}, // Only if includeRateLimits=true - "usage": {...} // Only if includeUsageData=true - }, - "links": { - "log": "/v1/logs/log_abc123", - "execution": "/v1/logs/executions/exec_def456" - } -} -``` - -### Webhook Headers - -Each webhook request includes these headers (webhook channel only): - -- `sim-event`: Event type (always `workflow.execution.completed`) -- `sim-timestamp`: Unix timestamp in milliseconds -- `sim-delivery-id`: Unique delivery ID for idempotency -- `sim-signature`: HMAC-SHA256 signature for verification (if secret configured) -- `Idempotency-Key`: Same as delivery ID for duplicate detection - -### Signature Verification - -If you configure a webhook secret, verify the signature to ensure the webhook is from Sim: - - - - ```javascript - import crypto from 'crypto'; - - function verifyWebhookSignature(body, signature, secret) { - const [timestampPart, signaturePart] = signature.split(','); - const timestamp = timestampPart.replace('t=', ''); - const expectedSignature = signaturePart.replace('v1=', ''); - - const signatureBase = `${timestamp}.${body}`; - const hmac = crypto.createHmac('sha256', secret); - hmac.update(signatureBase); - const computedSignature = hmac.digest('hex'); - - return computedSignature === expectedSignature; - } - - // In your webhook handler - app.post('/webhook', (req, res) => { - const signature = req.headers['sim-signature']; - const body = JSON.stringify(req.body); - - if (!verifyWebhookSignature(body, signature, process.env.WEBHOOK_SECRET)) { - return res.status(401).send('Invalid signature'); - } - - // Process the webhook... - }); - ``` - - - ```python - import hmac - import hashlib - import json - - def verify_webhook_signature(body: str, signature: str, secret: str) -> bool: - timestamp_part, signature_part = signature.split(',') - timestamp = timestamp_part.replace('t=', '') - expected_signature = signature_part.replace('v1=', '') - - signature_base = f"{timestamp}.{body}" - computed_signature = hmac.new( - secret.encode(), - signature_base.encode(), - hashlib.sha256 - ).hexdigest() - - return hmac.compare_digest(computed_signature, expected_signature) - - # In your webhook handler - @app.route('/webhook', methods=['POST']) - def webhook(): - signature = request.headers.get('sim-signature') - body = json.dumps(request.json) - - if not verify_webhook_signature(body, signature, os.environ['WEBHOOK_SECRET']): - return 'Invalid signature', 401 - - # Process the webhook... - ``` - - - -### Retry Policy - -Failed webhook deliveries are retried with exponential backoff and jitter: - -- Maximum attempts: 5 -- Retry delays: 5 seconds, 15 seconds, 1 minute, 3 minutes, 10 minutes -- Jitter: Up to 10% additional delay to prevent thundering herd -- Only HTTP 5xx and 429 responses trigger retries -- Deliveries timeout after 30 seconds - - - Webhook deliveries are processed asynchronously and don't affect workflow run performance. - - ## Best Practices 1. **Polling Strategy**: When polling for logs, use cursor-based pagination with `order=asc` and `startDate` to fetch new logs efficiently. -2. **Webhook Security**: Always configure a webhook secret and verify signatures to ensure requests are from Sim. +2. **Privacy**: By default, `finalOutput` and `traceSpans` are excluded from responses. Only enable these if you need the data and understand the privacy implications. -3. **Idempotency**: Use the `Idempotency-Key` header to detect and handle duplicate webhook deliveries. - -4. **Privacy**: By default, `finalOutput` and `traceSpans` are excluded from responses. Only enable these if you need the data and understand the privacy implications. - -5. **Rate Limiting**: Implement exponential backoff when you receive 429 responses. Check the `Retry-After` header for the recommended wait time. +3. **Rate Limiting**: Implement exponential backoff when you receive 429 responses. Check the `Retry-After` header for the recommended wait time. ## Rate Limiting @@ -540,67 +323,11 @@ async function pollLogs() { setInterval(pollLogs, 30000); ``` -## Example: Processing Webhooks - -```javascript -import express from 'express'; -import crypto from 'crypto'; - -const app = express(); -app.use(express.json()); - -app.post('/sim-webhook', (req, res) => { - // Verify signature - const signature = req.headers['sim-signature']; - const body = JSON.stringify(req.body); - - if (!verifyWebhookSignature(body, signature, process.env.WEBHOOK_SECRET)) { - return res.status(401).send('Invalid signature'); - } - - // Check timestamp to prevent replay attacks - const timestamp = parseInt(req.headers['sim-timestamp']); - const fiveMinutesAgo = Date.now() - (5 * 60 * 1000); - - if (timestamp < fiveMinutesAgo) { - return res.status(401).send('Timestamp too old'); - } - - // Process the webhook - const event = req.body; - - switch (event.type) { - case 'workflow.execution.completed': - const { workflowId, executionId, status, cost } = event.data; - - if (status === 'error') { - console.error(`Workflow ${workflowId} failed: ${executionId}`); - // Handle error... - } else { - console.log(`Workflow ${workflowId} completed: ${executionId}`); - console.log(`Cost: $${cost.total}`); - // Process successful execution... - } - break; - } - - // Return 200 to acknowledge receipt - res.status(200).send('OK'); -}); - -app.listen(3000, () => { - console.log('Webhook server listening on port 3000'); -}); -``` - import { FAQ } from '@/components/ui/faq' Sim Keys in the platform. Workflows with public API access enabled can also be called without authentication." }, - { question: "How does the webhook retry policy work?", answer: "Failed webhook deliveries are retried up to 5 times with exponential backoff: 5 seconds, 15 seconds, 1 minute, 3 minutes, and 10 minutes, plus up to 10% jitter. Only HTTP 5xx and 429 responses trigger retries. Each delivery times out after 30 seconds." }, { question: "What rate limits apply to the Logs API?", answer: "Rate limits use a token bucket algorithm. Free plans get 30 requests/minute with 60 burst capacity, Pro gets 100/200, Team gets 200/400, and Enterprise gets 500/1000. These are separate from workflow run rate limits, which are shown in the response body." }, - { question: "How do I verify that a webhook is from Sim?", answer: "Configure a webhook secret when setting up notifications. Sim signs each delivery with HMAC-SHA256 using the format 't={timestamp},v1={signature}' in the sim-signature header. Compute the HMAC of '{timestamp}.{body}' with your secret and compare it to the signature value." }, - { question: "What alert rules are available for notifications?", answer: "You can configure alerts for consecutive failures, failure rate thresholds, latency thresholds, latency spikes (percentage above average), cost thresholds, no-activity periods, and error counts within a time window. All alert types include a 1-hour cooldown to prevent notification spam." }, - { question: "Can I filter which runs trigger notifications?", answer: "Yes. You can filter notifications by specific workflows (or select all), log level (info or error), and trigger type (api, webhook, schedule, manual, chat). You can also choose whether to include final output, trace spans, rate limits, and usage data in the notification payload." }, + { question: "How do I get notified when a workflow run completes or fails?", answer: "Add the Sim trigger block to a workflow and deploy it. It runs on workspace events like execution successes, errors, deployments, and alert conditions such as latency spikes or cost thresholds — and you can compose any blocks downstream (Slack, email, webhooks, custom logic) to deliver the alert." }, ]} /> diff --git a/apps/docs/content/docs/en/execution/logging.mdx b/apps/docs/content/docs/en/execution/logging.mdx index dbbf50a3835..68a0ef17b6c 100644 --- a/apps/docs/content/docs/en/execution/logging.mdx +++ b/apps/docs/content/docs/en/execution/logging.mdx @@ -145,7 +145,7 @@ The snapshot provides: - Learn about [Cost Calculation](/execution/costs) to understand workflow pricing - Explore the [External API](/execution/api) for programmatic log access -- Set up [Notifications](/execution/api#notifications) for real-time alerts via webhook, email, or Slack +- Add the Sim trigger block to a workflow to react to execution errors, successes, deployments, and alert conditions in real time import { FAQ } from '@/components/ui/faq' @@ -154,6 +154,6 @@ import { FAQ } from '@/components/ui/faq' { question: "What data is captured in each run log?", answer: "Each log entry includes the run ID, workflow ID, trigger type, start and end timestamps, total duration in milliseconds, cost breakdown (total cost, token counts, and per-model breakdowns), run data with trace spans, final output, and any associated files. The log details sidebar lets you inspect block-level inputs and outputs." }, { question: "Are API keys visible in the logs?", answer: "No. API keys and credentials are automatically redacted in the log input tab for security. You can safely inspect block inputs without exposing sensitive values." }, { question: "What is a workflow snapshot?", answer: "A workflow snapshot is a frozen copy of the workflow's structure (blocks, connections, and configuration) captured at the time of a run. It lets you see the exact state of the workflow when a particular run happened, which is useful for debugging workflows that have been modified since." }, - { question: "Can I access logs programmatically?", answer: "Yes. The External API provides endpoints to query logs with filtering by workflow, time range, trigger type, duration, cost, and model. You can also set up webhook, email, or Slack notifications for real-time alerts when runs complete." }, + { question: "Can I access logs programmatically?", answer: "Yes. The External API provides endpoints to query logs with filtering by workflow, time range, trigger type, duration, cost, and model. To react to runs in real time, use the Sim trigger block to run a workflow on execution events." }, { question: "What does Live mode do on the Logs page?", answer: "Live mode automatically refreshes the Logs page in real-time so new log entries appear as they are recorded, without requiring manual page refreshes. This is useful during deployments or when monitoring active workflows." }, ]} /> \ No newline at end of file diff --git a/apps/docs/content/docs/en/tools/meta.json b/apps/docs/content/docs/en/tools/meta.json index c2edcceb166..e0b089a1c58 100644 --- a/apps/docs/content/docs/en/tools/meta.json +++ b/apps/docs/content/docs/en/tools/meta.json @@ -48,7 +48,6 @@ "elevenlabs", "emailbison", "enrich", - "enrichment", "evernote", "exa", "extend", @@ -120,6 +119,7 @@ "mistral_parse", "monday", "mongodb", + "mysql", "neo4j", "neverbounce", "new_relic", @@ -137,6 +137,7 @@ "pinecone", "pipedrive", "polymarket", + "postgresql", "posthog", "profound", "prospeo", @@ -164,12 +165,15 @@ "serper", "servicenow", "ses", + "sftp", "sharepoint", "shopify", "similarweb", "sixtyfour", "slack", + "smtp", "sqs", + "ssh", "stagehand", "stripe", "sts", diff --git a/apps/docs/content/docs/en/tools/mysql.mdx b/apps/docs/content/docs/en/tools/mysql.mdx new file mode 100644 index 00000000000..b5bb354b37f --- /dev/null +++ b/apps/docs/content/docs/en/tools/mysql.mdx @@ -0,0 +1,168 @@ +--- +title: MySQL +description: Connect to MySQL database +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + +## Usage Instructions + +Integrate MySQL into the workflow. Can query, insert, update, delete, and execute raw SQL. + + + +## Tools + +### `mysql_query` + +Execute SELECT query on MySQL database + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `host` | string | Yes | MySQL server hostname or IP address | +| `port` | number | Yes | MySQL server port \(default: 3306\) | +| `database` | string | Yes | Database name to connect to \(e.g., my_database\) | +| `username` | string | Yes | Database username | +| `password` | string | Yes | Database password | +| `ssl` | string | No | SSL connection mode \(disabled, required, preferred\) | +| `query` | string | Yes | SQL SELECT query to execute \(e.g., SELECT * FROM users WHERE active = 1\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Operation status message | +| `rows` | array | Array of rows returned from the query | +| `rowCount` | number | Number of rows returned | + +### `mysql_insert` + +Insert new record into MySQL database + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `host` | string | Yes | MySQL server hostname or IP address | +| `port` | number | Yes | MySQL server port \(default: 3306\) | +| `database` | string | Yes | Database name to connect to \(e.g., my_database\) | +| `username` | string | Yes | Database username | +| `password` | string | Yes | Database password | +| `ssl` | string | No | SSL connection mode \(disabled, required, preferred\) | +| `table` | string | Yes | Table name to insert into \(e.g., users, orders\) | +| `data` | object | Yes | Data to insert as key-value pairs | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Operation status message | +| `rows` | array | Array of inserted rows | +| `rowCount` | number | Number of rows inserted | + +### `mysql_update` + +Update existing records in MySQL database + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `host` | string | Yes | MySQL server hostname or IP address | +| `port` | number | Yes | MySQL server port \(default: 3306\) | +| `database` | string | Yes | Database name to connect to \(e.g., my_database\) | +| `username` | string | Yes | Database username | +| `password` | string | Yes | Database password | +| `ssl` | string | No | SSL connection mode \(disabled, required, preferred\) | +| `table` | string | Yes | Table name to update \(e.g., users, orders\) | +| `data` | object | Yes | Data to update as key-value pairs | +| `where` | string | Yes | WHERE clause condition \(without WHERE keyword\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Operation status message | +| `rows` | array | Array of updated rows | +| `rowCount` | number | Number of rows updated | + +### `mysql_delete` + +Delete records from MySQL database + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `host` | string | Yes | MySQL server hostname or IP address | +| `port` | number | Yes | MySQL server port \(default: 3306\) | +| `database` | string | Yes | Database name to connect to \(e.g., my_database\) | +| `username` | string | Yes | Database username | +| `password` | string | Yes | Database password | +| `ssl` | string | No | SSL connection mode \(disabled, required, preferred\) | +| `table` | string | Yes | Table name to delete from \(e.g., users, orders\) | +| `where` | string | Yes | WHERE clause condition \(without WHERE keyword\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Operation status message | +| `rows` | array | Array of deleted rows | +| `rowCount` | number | Number of rows deleted | + +### `mysql_execute` + +Execute raw SQL query on MySQL database + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `host` | string | Yes | MySQL server hostname or IP address | +| `port` | number | Yes | MySQL server port \(default: 3306\) | +| `database` | string | Yes | Database name to connect to \(e.g., my_database\) | +| `username` | string | Yes | Database username | +| `password` | string | Yes | Database password | +| `ssl` | string | No | SSL connection mode \(disabled, required, preferred\) | +| `query` | string | Yes | Raw SQL query to execute \(e.g., CREATE TABLE users \(id INT PRIMARY KEY, name VARCHAR\(255\)\)\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Operation status message | +| `rows` | array | Array of rows returned from the query | +| `rowCount` | number | Number of rows affected | + +### `mysql_introspect` + +Introspect MySQL database schema to retrieve table structures, columns, and relationships + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `host` | string | Yes | MySQL server hostname or IP address | +| `port` | number | Yes | MySQL server port \(default: 3306\) | +| `database` | string | Yes | Database name to connect to \(e.g., my_database\) | +| `username` | string | Yes | Database username | +| `password` | string | Yes | Database password | +| `ssl` | string | No | SSL connection mode \(disabled, required, preferred\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Operation status message | +| `tables` | array | Array of table schemas with columns, keys, and indexes | +| `databases` | array | List of available databases on the server | + + diff --git a/apps/docs/content/docs/en/tools/postgresql.mdx b/apps/docs/content/docs/en/tools/postgresql.mdx new file mode 100644 index 00000000000..b2099d51eca --- /dev/null +++ b/apps/docs/content/docs/en/tools/postgresql.mdx @@ -0,0 +1,190 @@ +--- +title: PostgreSQL +description: Connect to PostgreSQL database +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + +## Usage Instructions + +Integrate PostgreSQL into the workflow. Can query, insert, update, delete, and execute raw SQL. + + + +## Tools + +### `postgresql_query` + +Execute a SELECT query on PostgreSQL database + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `host` | string | Yes | PostgreSQL server hostname or IP address | +| `port` | number | Yes | PostgreSQL server port \(default: 5432\) | +| `database` | string | Yes | Database name to connect to | +| `username` | string | Yes | Database username | +| `password` | string | Yes | Database password | +| `ssl` | string | No | SSL connection mode \(disabled, required, preferred\) | +| `query` | string | Yes | SQL SELECT query to execute | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Operation status message | +| `rows` | array | Array of rows returned from the query | +| `rowCount` | number | Number of rows returned | + +### `postgresql_insert` + +Insert data into PostgreSQL database + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `host` | string | Yes | PostgreSQL server hostname or IP address | +| `port` | number | Yes | PostgreSQL server port \(default: 5432\) | +| `database` | string | Yes | Database name to connect to | +| `username` | string | Yes | Database username | +| `password` | string | Yes | Database password | +| `ssl` | string | No | SSL connection mode \(disabled, required, preferred\) | +| `table` | string | Yes | Table name to insert data into | +| `data` | object | Yes | Data object to insert \(key-value pairs\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Operation status message | +| `rows` | array | Inserted data \(if RETURNING clause used\) | +| `rowCount` | number | Number of rows inserted | + +### `postgresql_update` + +Update data in PostgreSQL database + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `host` | string | Yes | PostgreSQL server hostname or IP address | +| `port` | number | Yes | PostgreSQL server port \(default: 5432\) | +| `database` | string | Yes | Database name to connect to | +| `username` | string | Yes | Database username | +| `password` | string | Yes | Database password | +| `ssl` | string | No | SSL connection mode \(disabled, required, preferred\) | +| `table` | string | Yes | Table name to update data in | +| `data` | object | Yes | Data object with fields to update \(key-value pairs\) | +| `where` | string | Yes | WHERE clause condition \(without WHERE keyword\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Operation status message | +| `rows` | array | Updated data \(if RETURNING clause used\) | +| `rowCount` | number | Number of rows updated | + +### `postgresql_delete` + +Delete data from PostgreSQL database + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `host` | string | Yes | PostgreSQL server hostname or IP address | +| `port` | number | Yes | PostgreSQL server port \(default: 5432\) | +| `database` | string | Yes | Database name to connect to | +| `username` | string | Yes | Database username | +| `password` | string | Yes | Database password | +| `ssl` | string | No | SSL connection mode \(disabled, required, preferred\) | +| `table` | string | Yes | Table name to delete data from | +| `where` | string | Yes | WHERE clause condition \(without WHERE keyword\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Operation status message | +| `rows` | array | Deleted data \(if RETURNING clause used\) | +| `rowCount` | number | Number of rows deleted | + +### `postgresql_execute` + +Execute raw SQL query on PostgreSQL database + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `host` | string | Yes | PostgreSQL server hostname or IP address | +| `port` | number | Yes | PostgreSQL server port \(default: 5432\) | +| `database` | string | Yes | Database name to connect to | +| `username` | string | Yes | Database username | +| `password` | string | Yes | Database password | +| `ssl` | string | No | SSL connection mode \(disabled, required, preferred\) | +| `query` | string | Yes | Raw SQL query to execute | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Operation status message | +| `rows` | array | Array of rows returned from the query | +| `rowCount` | number | Number of rows affected | + +### `postgresql_introspect` + +Introspect PostgreSQL database schema to retrieve table structures, columns, and relationships + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `host` | string | Yes | PostgreSQL server hostname or IP address | +| `port` | number | Yes | PostgreSQL server port \(default: 5432\) | +| `database` | string | Yes | Database name to connect to | +| `username` | string | Yes | Database username | +| `password` | string | Yes | Database password | +| `ssl` | string | No | SSL connection mode \(disabled, required, preferred\) | +| `schema` | string | No | Schema to introspect \(default: public\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Operation status message | +| `tables` | array | Array of table schemas with columns, keys, and indexes | +| ↳ `name` | string | Table name | +| ↳ `schema` | string | Schema name \(e.g., public\) | +| ↳ `columns` | array | Table columns | +| ↳ `name` | string | Column name | +| ↳ `type` | string | Data type \(e.g., integer, varchar, timestamp\) | +| ↳ `nullable` | boolean | Whether the column allows NULL values | +| ↳ `default` | string | Default value expression | +| ↳ `isPrimaryKey` | boolean | Whether the column is part of the primary key | +| ↳ `isForeignKey` | boolean | Whether the column is a foreign key | +| ↳ `references` | object | Foreign key reference information | +| ↳ `table` | string | Referenced table name | +| ↳ `column` | string | Referenced column name | +| ↳ `primaryKey` | array | Primary key column names | +| ↳ `foreignKeys` | array | Foreign key constraints | +| ↳ `column` | string | Local column name | +| ↳ `referencesTable` | string | Referenced table name | +| ↳ `referencesColumn` | string | Referenced column name | +| ↳ `indexes` | array | Table indexes | +| ↳ `name` | string | Index name | +| ↳ `columns` | array | Columns included in the index | +| ↳ `unique` | boolean | Whether the index enforces uniqueness | +| `schemas` | array | List of available schemas in the database | + + diff --git a/apps/docs/content/docs/en/tools/servicenow.mdx b/apps/docs/content/docs/en/tools/servicenow.mdx index 398e3fd6db2..7337cbf636b 100644 --- a/apps/docs/content/docs/en/tools/servicenow.mdx +++ b/apps/docs/content/docs/en/tools/servicenow.mdx @@ -215,7 +215,6 @@ Attach a file to a ServiceNow record | `recordSysId` | string | Yes | sys_id of the record to attach the file to | | `fileName` | string | Yes | Name to give the uploaded file \(e.g., logs.txt\) | | `file` | file | No | File to upload \(UserFile object\) | -| `fileContent` | string | No | Base64-encoded file content \(legacy\) | #### Output diff --git a/apps/docs/content/docs/en/tools/sftp.mdx b/apps/docs/content/docs/en/tools/sftp.mdx new file mode 100644 index 00000000000..b350597fcc7 --- /dev/null +++ b/apps/docs/content/docs/en/tools/sftp.mdx @@ -0,0 +1,156 @@ +--- +title: SFTP +description: Transfer files via SFTP (SSH File Transfer Protocol) +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + +## Usage Instructions + +Upload, download, list, and manage files on remote servers via SFTP. Supports both password and private key authentication for secure file transfers. + + + +## Tools + +### `sftp_upload` + +Upload files to a remote SFTP server + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `host` | string | Yes | SFTP server hostname or IP address | +| `port` | number | Yes | SFTP server port \(default: 22\) | +| `username` | string | Yes | SFTP username | +| `password` | string | No | Password for authentication \(if not using private key\) | +| `privateKey` | string | No | Private key for authentication \(OpenSSH format\) | +| `passphrase` | string | No | Passphrase for encrypted private key | +| `remotePath` | string | Yes | Destination directory on the remote server | +| `files` | file[] | No | Files to upload | +| `fileContent` | string | No | Direct file content to upload \(for text files\) | +| `fileName` | string | No | File name when using direct content | +| `overwrite` | boolean | No | Whether to overwrite existing files \(default: true\) | +| `permissions` | string | No | File permissions \(e.g., 0644\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the upload was successful | +| `uploadedFiles` | json | Array of uploaded file details \(name, remotePath, size\) | +| `message` | string | Operation status message | + +### `sftp_download` + +Download a file from a remote SFTP server + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `host` | string | Yes | SFTP server hostname or IP address | +| `port` | number | Yes | SFTP server port \(default: 22\) | +| `username` | string | Yes | SFTP username | +| `password` | string | No | Password for authentication \(if not using private key\) | +| `privateKey` | string | No | Private key for authentication \(OpenSSH format\) | +| `passphrase` | string | No | Passphrase for encrypted private key | +| `remotePath` | string | Yes | Path to the file on the remote server | +| `encoding` | string | No | Output encoding: utf-8 for text, base64 for binary \(default: utf-8\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the download was successful | +| `file` | file | Downloaded file stored in execution files | +| `fileName` | string | Name of the downloaded file | +| `content` | string | File content \(text or base64 encoded\) | +| `size` | number | File size in bytes | +| `encoding` | string | Content encoding \(utf-8 or base64\) | +| `message` | string | Operation status message | + +### `sftp_list` + +List files and directories on a remote SFTP server + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `host` | string | Yes | SFTP server hostname or IP address | +| `port` | number | Yes | SFTP server port \(default: 22\) | +| `username` | string | Yes | SFTP username | +| `password` | string | No | Password for authentication \(if not using private key\) | +| `privateKey` | string | No | Private key for authentication \(OpenSSH format\) | +| `passphrase` | string | No | Passphrase for encrypted private key | +| `remotePath` | string | Yes | Directory path on the remote server | +| `detailed` | boolean | No | Include detailed file information \(size, permissions, modified date\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the operation was successful | +| `path` | string | Directory path that was listed | +| `entries` | json | Array of directory entries with name, type, size, permissions, modifiedAt | +| `count` | number | Number of entries in the directory | +| `message` | string | Operation status message | + +### `sftp_delete` + +Delete a file or directory on a remote SFTP server + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `host` | string | Yes | SFTP server hostname or IP address | +| `port` | number | Yes | SFTP server port \(default: 22\) | +| `username` | string | Yes | SFTP username | +| `password` | string | No | Password for authentication \(if not using private key\) | +| `privateKey` | string | No | Private key for authentication \(OpenSSH format\) | +| `passphrase` | string | No | Passphrase for encrypted private key | +| `remotePath` | string | Yes | Path to the file or directory to delete | +| `recursive` | boolean | No | Delete directories recursively | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the deletion was successful | +| `deletedPath` | string | Path that was deleted | +| `message` | string | Operation status message | + +### `sftp_mkdir` + +Create a directory on a remote SFTP server + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `host` | string | Yes | SFTP server hostname or IP address | +| `port` | number | Yes | SFTP server port \(default: 22\) | +| `username` | string | Yes | SFTP username | +| `password` | string | No | Password for authentication \(if not using private key\) | +| `privateKey` | string | No | Private key for authentication \(OpenSSH format\) | +| `passphrase` | string | No | Passphrase for encrypted private key | +| `remotePath` | string | Yes | Path for the new directory | +| `recursive` | boolean | No | Create parent directories if they do not exist | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the directory was created successfully | +| `createdPath` | string | Path of the created directory | +| `message` | string | Operation status message | + + diff --git a/apps/docs/content/docs/en/tools/smtp.mdx b/apps/docs/content/docs/en/tools/smtp.mdx new file mode 100644 index 00000000000..ab57c300c56 --- /dev/null +++ b/apps/docs/content/docs/en/tools/smtp.mdx @@ -0,0 +1,55 @@ +--- +title: SMTP +description: Send emails via any SMTP mail server +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + +## Usage Instructions + +Send emails using any SMTP server (Gmail, Outlook, custom servers, etc.). Configure SMTP connection settings and send emails with full control over content, recipients, and attachments. + + + +## Tools + +### `smtp_send_mail` + +Send emails via SMTP server + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `smtpHost` | string | Yes | SMTP server hostname \(e.g., smtp.gmail.com\) | +| `smtpPort` | number | Yes | SMTP server port \(587 for TLS, 465 for SSL\) | +| `smtpUsername` | string | Yes | SMTP authentication username | +| `smtpPassword` | string | Yes | SMTP authentication password | +| `smtpSecure` | string | Yes | Security protocol \(TLS, SSL, or None\) | +| `from` | string | Yes | Sender email address | +| `to` | string | Yes | Recipient email address | +| `subject` | string | Yes | Email subject | +| `body` | string | Yes | Email body content | +| `contentType` | string | No | Content type \(text or html\) | +| `fromName` | string | No | Display name for sender | +| `cc` | string | No | CC recipients \(comma-separated\) | +| `bcc` | string | No | BCC recipients \(comma-separated\) | +| `replyTo` | string | No | Reply-to email address | +| `attachments` | file[] | No | Files to attach to the email | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the email was sent successfully | +| `messageId` | string | Message ID from SMTP server | +| `to` | string | Recipient email address | +| `subject` | string | Email subject | +| `error` | string | Error message if sending failed | + + diff --git a/apps/docs/content/docs/en/tools/ssh.mdx b/apps/docs/content/docs/en/tools/ssh.mdx new file mode 100644 index 00000000000..6fc4a66bb56 --- /dev/null +++ b/apps/docs/content/docs/en/tools/ssh.mdx @@ -0,0 +1,382 @@ +--- +title: SSH +description: Connect to remote servers via SSH +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + +## Usage Instructions + +Execute commands, transfer files, and manage remote servers via SSH. Supports password and private key authentication for secure server access. + + + +## Tools + +### `ssh_execute_command` + +Execute a shell command on a remote SSH server + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `host` | string | Yes | SSH server hostname or IP address | +| `port` | number | Yes | SSH server port \(default: 22\) | +| `username` | string | Yes | SSH username | +| `password` | string | No | Password for authentication \(if not using private key\) | +| `privateKey` | string | No | Private key for authentication \(OpenSSH format\) | +| `passphrase` | string | No | Passphrase for encrypted private key | +| `command` | string | Yes | Shell command to execute on the remote server | +| `workingDirectory` | string | No | Working directory for command execution | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `stdout` | string | Standard output from command | +| `stderr` | string | Standard error output | +| `exitCode` | number | Command exit code | +| `success` | boolean | Whether command succeeded \(exit code 0\) | +| `message` | string | Operation status message | + +### `ssh_execute_script` + +Upload and execute a multi-line script on a remote SSH server + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `host` | string | Yes | SSH server hostname or IP address | +| `port` | number | Yes | SSH server port \(default: 22\) | +| `username` | string | Yes | SSH username | +| `password` | string | No | Password for authentication \(if not using private key\) | +| `privateKey` | string | No | Private key for authentication \(OpenSSH format\) | +| `passphrase` | string | No | Passphrase for encrypted private key | +| `script` | string | Yes | Script content to execute \(bash, python, etc.\) | +| `interpreter` | string | No | Script interpreter \(default: /bin/bash\) | +| `workingDirectory` | string | No | Working directory for script execution | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `stdout` | string | Standard output from script | +| `stderr` | string | Standard error output | +| `exitCode` | number | Script exit code | +| `success` | boolean | Whether script succeeded \(exit code 0\) | +| `scriptPath` | string | Temporary path where script was uploaded | +| `message` | string | Operation status message | + +### `ssh_check_command_exists` + +Check if a command/program exists on the remote SSH server + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `host` | string | Yes | SSH server hostname or IP address | +| `port` | number | Yes | SSH server port \(default: 22\) | +| `username` | string | Yes | SSH username | +| `password` | string | No | Password for authentication \(if not using private key\) | +| `privateKey` | string | No | Private key for authentication \(OpenSSH format\) | +| `passphrase` | string | No | Passphrase for encrypted private key | +| `commandName` | string | Yes | Command name to check \(e.g., docker, git, python3\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `commandExists` | boolean | Whether the command exists | +| `commandPath` | string | Full path to the command \(if found\) | +| `version` | string | Command version output \(if applicable\) | +| `message` | string | Operation status message | + +### `ssh_upload_file` + +Upload a file to a remote SSH server + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `host` | string | Yes | SSH server hostname or IP address | +| `port` | number | Yes | SSH server port \(default: 22\) | +| `username` | string | Yes | SSH username | +| `password` | string | No | Password for authentication \(if not using private key\) | +| `privateKey` | string | No | Private key for authentication \(OpenSSH format\) | +| `passphrase` | string | No | Passphrase for encrypted private key | +| `fileContent` | string | Yes | File content to upload \(base64 encoded for binary files\) | +| `fileName` | string | Yes | Name of the file being uploaded | +| `remotePath` | string | Yes | Destination path on the remote server | +| `permissions` | string | No | File permissions \(e.g., 0644\) | +| `overwrite` | boolean | No | Whether to overwrite existing files \(default: true\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `uploaded` | boolean | Whether the file was uploaded successfully | +| `remotePath` | string | Final path on the remote server | +| `size` | number | File size in bytes | +| `message` | string | Operation status message | + +### `ssh_download_file` + +Download a file from a remote SSH server + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `host` | string | Yes | SSH server hostname or IP address | +| `port` | number | Yes | SSH server port \(default: 22\) | +| `username` | string | Yes | SSH username | +| `password` | string | No | Password for authentication \(if not using private key\) | +| `privateKey` | string | No | Private key for authentication \(OpenSSH format\) | +| `passphrase` | string | No | Passphrase for encrypted private key | +| `remotePath` | string | Yes | Path of the file on the remote server | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `downloaded` | boolean | Whether the file was downloaded successfully | +| `file` | file | Downloaded file stored in execution files | +| `fileContent` | string | File content \(base64 encoded for binary files\) | +| `fileName` | string | Name of the downloaded file | +| `remotePath` | string | Source path on the remote server | +| `size` | number | File size in bytes | +| `message` | string | Operation status message | + +### `ssh_list_directory` + +List files and directories in a remote directory + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `host` | string | Yes | SSH server hostname or IP address | +| `port` | number | Yes | SSH server port \(default: 22\) | +| `username` | string | Yes | SSH username | +| `password` | string | No | Password for authentication \(if not using private key\) | +| `privateKey` | string | No | Private key for authentication \(OpenSSH format\) | +| `passphrase` | string | No | Passphrase for encrypted private key | +| `path` | string | Yes | Remote directory path to list | +| `detailed` | boolean | No | Include file details \(size, permissions, modified date\) | +| `recursive` | boolean | No | List subdirectories recursively \(default: false\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `entries` | array | Array of file and directory entries | +| ↳ `name` | string | File or directory name | +| ↳ `type` | string | Entry type \(file, directory, symlink\) | +| ↳ `size` | number | File size in bytes | +| ↳ `permissions` | string | File permissions | +| ↳ `modified` | string | Last modified timestamp | +| `totalFiles` | number | Total number of files | +| `totalDirectories` | number | Total number of directories | +| `message` | string | Operation status message | + +### `ssh_check_file_exists` + +Check if a file or directory exists on the remote SSH server + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `host` | string | Yes | SSH server hostname or IP address | +| `port` | number | Yes | SSH server port \(default: 22\) | +| `username` | string | Yes | SSH username | +| `password` | string | No | Password for authentication \(if not using private key\) | +| `privateKey` | string | No | Private key for authentication \(OpenSSH format\) | +| `passphrase` | string | No | Passphrase for encrypted private key | +| `path` | string | Yes | Remote file or directory path to check | +| `type` | string | No | Expected type: file, directory, or any \(default: any\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `exists` | boolean | Whether the path exists | +| `type` | string | Type of path \(file, directory, symlink, not_found\) | +| `size` | number | File size if it is a file | +| `permissions` | string | File permissions \(e.g., 0755\) | +| `modified` | string | Last modified timestamp | +| `message` | string | Operation status message | + +### `ssh_create_directory` + +Create a directory on the remote SSH server + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `host` | string | Yes | SSH server hostname or IP address | +| `port` | number | Yes | SSH server port \(default: 22\) | +| `username` | string | Yes | SSH username | +| `password` | string | No | Password for authentication \(if not using private key\) | +| `privateKey` | string | No | Private key for authentication \(OpenSSH format\) | +| `passphrase` | string | No | Passphrase for encrypted private key | +| `path` | string | Yes | Directory path to create | +| `recursive` | boolean | No | Create parent directories if they do not exist \(default: true\) | +| `permissions` | string | No | Directory permissions \(default: 0755\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `created` | boolean | Whether the directory was created successfully | +| `remotePath` | string | Created directory path | +| `alreadyExists` | boolean | Whether the directory already existed | +| `message` | string | Operation status message | + +### `ssh_delete_file` + +Delete a file or directory from the remote SSH server + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `host` | string | Yes | SSH server hostname or IP address | +| `port` | number | Yes | SSH server port \(default: 22\) | +| `username` | string | Yes | SSH username | +| `password` | string | No | Password for authentication \(if not using private key\) | +| `privateKey` | string | No | Private key for authentication \(OpenSSH format\) | +| `passphrase` | string | No | Passphrase for encrypted private key | +| `path` | string | Yes | Path to delete | +| `recursive` | boolean | No | Recursively delete directories \(default: false\) | +| `force` | boolean | No | Force deletion without confirmation \(default: false\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `deleted` | boolean | Whether the path was deleted successfully | +| `remotePath` | string | Deleted path | +| `message` | string | Operation status message | + +### `ssh_move_rename` + +Move or rename a file or directory on the remote SSH server + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `host` | string | Yes | SSH server hostname or IP address | +| `port` | number | Yes | SSH server port \(default: 22\) | +| `username` | string | Yes | SSH username | +| `password` | string | No | Password for authentication \(if not using private key\) | +| `privateKey` | string | No | Private key for authentication \(OpenSSH format\) | +| `passphrase` | string | No | Passphrase for encrypted private key | +| `sourcePath` | string | Yes | Current path of the file or directory | +| `destinationPath` | string | Yes | New path for the file or directory | +| `overwrite` | boolean | No | Overwrite destination if it exists \(default: false\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `moved` | boolean | Whether the operation was successful | +| `sourcePath` | string | Original path | +| `destinationPath` | string | New path | +| `message` | string | Operation status message | + +### `ssh_get_system_info` + +Retrieve system information from the remote SSH server + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `host` | string | Yes | SSH server hostname or IP address | +| `port` | number | Yes | SSH server port \(default: 22\) | +| `username` | string | Yes | SSH username | +| `password` | string | No | Password for authentication \(if not using private key\) | +| `privateKey` | string | No | Private key for authentication \(OpenSSH format\) | +| `passphrase` | string | No | Passphrase for encrypted private key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `hostname` | string | Server hostname | +| `os` | string | Operating system \(e.g., Linux, Darwin\) | +| `architecture` | string | CPU architecture \(e.g., x64, arm64\) | +| `uptime` | number | System uptime in seconds | +| `memory` | json | Memory information \(total, free, used\) | +| `diskSpace` | json | Disk space information \(total, free, used\) | +| `message` | string | Operation status message | + +### `ssh_read_file_content` + +Read the contents of a remote file + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `host` | string | Yes | SSH server hostname or IP address | +| `port` | number | Yes | SSH server port \(default: 22\) | +| `username` | string | Yes | SSH username | +| `password` | string | No | Password for authentication \(if not using private key\) | +| `privateKey` | string | No | Private key for authentication \(OpenSSH format\) | +| `passphrase` | string | No | Passphrase for encrypted private key | +| `path` | string | Yes | Remote file path to read | +| `encoding` | string | No | File encoding \(default: utf-8\) | +| `maxSize` | number | No | Maximum file size to read in MB \(default: 10\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `content` | string | File content as string | +| `size` | number | File size in bytes | +| `lines` | number | Number of lines in file | +| `remotePath` | string | Remote file path | +| `message` | string | Operation status message | + +### `ssh_write_file_content` + +Write or append content to a remote file + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `host` | string | Yes | SSH server hostname or IP address | +| `port` | number | Yes | SSH server port \(default: 22\) | +| `username` | string | Yes | SSH username | +| `password` | string | No | Password for authentication \(if not using private key\) | +| `privateKey` | string | No | Private key for authentication \(OpenSSH format\) | +| `passphrase` | string | No | Passphrase for encrypted private key | +| `path` | string | Yes | Remote file path to write to | +| `content` | string | Yes | Content to write to the file | +| `mode` | string | No | Write mode: overwrite, append, or create \(default: overwrite\) | +| `permissions` | string | No | File permissions \(e.g., 0644\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `written` | boolean | Whether the file was written successfully | +| `remotePath` | string | File path | +| `size` | number | Final file size in bytes | +| `message` | string | Operation status message | + + diff --git a/apps/docs/content/docs/en/triggers/meta.json b/apps/docs/content/docs/en/triggers/meta.json index 10a41e72ef0..115514f4976 100644 --- a/apps/docs/content/docs/en/triggers/meta.json +++ b/apps/docs/content/docs/en/triggers/meta.json @@ -5,6 +5,7 @@ "schedule", "webhook", "rss", + "sim", "airtable", "ashby", "attio", diff --git a/apps/docs/content/docs/en/triggers/sim.mdx b/apps/docs/content/docs/en/triggers/sim.mdx new file mode 100644 index 00000000000..c46b54ed5a4 --- /dev/null +++ b/apps/docs/content/docs/en/triggers/sim.mdx @@ -0,0 +1,85 @@ +--- +title: Sim +--- + +import { Callout } from 'fumadocs-ui/components/callout' +import { Tab, Tabs } from 'fumadocs-ui/components/tabs' +import { FAQ } from '@/components/ui/faq' + +The Sim trigger runs a workflow when events happen in your workspace: another workflow's run fails or succeeds, a workflow is deployed, or an alert condition like a latency spike or cost threshold is met. Use it to build side-effect workflows — alerting, escalation, auto-remediation — composed from any blocks (Slack, email, webhooks, custom logic). + +## Events + +Pick one event per Sim trigger block: + +**Plain events** — fire on every occurrence: + +
    +
  • Execution Error: a watched workflow's run failed
  • +
  • Execution Success: a watched workflow's run completed successfully
  • +
  • Workflow Deployed: a watched workflow was deployed (including redeploys and version rollbacks)
  • +
+ +**Alert conditions** — evaluated as runs complete (failure-based conditions evaluate on failed runs), with a cooldown so they fire at most once per cooldown window: + +
    +
  • Consecutive Failures: the last N runs all failed
  • +
  • Failure Rate: failure rate meets or exceeds a percentage over a time window (minimum 5 runs)
  • +
  • Latency Threshold: a run took longer than a fixed duration
  • +
  • Latency Spike: a run was a configurable percentage slower than the recent average (minimum 5 runs)
  • +
  • Cost Threshold: a run cost more than a credit threshold
  • +
  • Error Count: N or more errors occurred within a time window
  • +
  • No Activity: a watched workflow had no runs for a configurable number of hours
  • +
+ +## Workflow Scope + +By default the trigger watches every workflow in the workspace. Select specific workflows to narrow it. The workflow containing the trigger is always excluded — it never receives events about itself. + +## Outputs + +All events include `event`, `timestamp`, `workflowId`, and `workflowName` (the source workflow). Additional fields depend on the event type: + + + +
    +
  • runId: the run that completed
  • +
  • durationMs: run duration in milliseconds
  • +
  • cost: run cost in credits
  • +
  • finalOutput: the run's final output (truncated when large)
  • +
+
+ +

Run-backed conditions nest the run that tripped them under triggeringRun:

+
    +
  • triggeringRun.runId, triggeringRun.durationMs, triggeringRun.cost, triggeringRun.finalOutput
  • +
+

No Activity has no triggering run, so it carries only the base fields.

+
+ +
    +
  • version: the deployment version number that was activated
  • +
+
+
+ +## Behavior + +
    +
  • The workflow containing the Sim trigger must be deployed for events to fire.
  • +
  • Runs started by a Sim trigger never emit workspace events, so side-effect workflows cannot chain or loop.
  • +
  • Alert conditions fire at most once per cooldown window (one hour, or the inactivity window for No Activity).
  • +
  • Event delivery is fire-and-forget: side-effect runs are billed like any other run and are subject to workspace rate limits.
  • +
+ + + Trigger configuration is snapshotted at deploy time. After changing the event type, workflow + scope, or thresholds, redeploy the workflow for the changes to take effect. + + + diff --git a/apps/sim/app/api/emails/preview/route.ts b/apps/sim/app/api/emails/preview/route.ts index 7459448fb98..32af9724073 100644 --- a/apps/sim/app/api/emails/preview/route.ts +++ b/apps/sim/app/api/emails/preview/route.ts @@ -13,7 +13,6 @@ import { renderPlanWelcomeEmail, renderUsageThresholdEmail, renderWelcomeEmail, - renderWorkflowNotificationEmail, renderWorkspaceInvitationEmail, } from '@/components/emails' import { emailPreviewQuerySchema } from '@/lib/api/contracts/common' @@ -94,51 +93,6 @@ const emailTemplates = { billingPortalUrl: 'https://sim.ai/settings/billing', failureReason: 'Card declined', }), - - // Notification emails - 'workflow-notification-success': () => - renderWorkflowNotificationEmail({ - workflowName: 'Customer Onboarding Flow', - status: 'success', - trigger: 'api', - duration: '2.3s', - cost: '$0.0042', - logUrl: 'https://sim.ai/workspace/ws_123/logs?executionId=exec_abc123', - }), - 'workflow-notification-error': () => - renderWorkflowNotificationEmail({ - workflowName: 'Customer Onboarding Flow', - status: 'error', - trigger: 'webhook', - duration: '1.1s', - cost: '$0.0021', - logUrl: 'https://sim.ai/workspace/ws_123/logs?executionId=exec_abc123', - }), - 'workflow-notification-alert': () => - renderWorkflowNotificationEmail({ - workflowName: 'Customer Onboarding Flow', - status: 'error', - trigger: 'schedule', - duration: '45.2s', - cost: '$0.0156', - logUrl: 'https://sim.ai/workspace/ws_123/logs?executionId=exec_abc123', - alertReason: '3 consecutive failures detected', - }), - 'workflow-notification-full': () => - renderWorkflowNotificationEmail({ - workflowName: 'Data Processing Pipeline', - status: 'success', - trigger: 'api', - duration: '12.5s', - cost: '$0.0234', - logUrl: 'https://sim.ai/workspace/ws_123/logs?executionId=exec_abc123', - finalOutput: { processed: 150, skipped: 3, status: 'completed' }, - rateLimits: { - sync: { requestsPerMinute: 60, remaining: 45 }, - async: { requestsPerMinute: 120, remaining: 98 }, - }, - usageData: { currentPeriodCost: 12.45, limit: 50, percentUsed: 24.9 }, - }), } as const type EmailTemplate = keyof typeof emailTemplates @@ -169,12 +123,6 @@ export const GET = withRouteHandler(async (request: NextRequest) => { 'credit-purchase', 'payment-failed', ], - Notifications: [ - 'workflow-notification-success', - 'workflow-notification-error', - 'workflow-notification-alert', - 'workflow-notification-full', - ], } const categoryHtml = Object.entries(categories) diff --git a/apps/sim/app/api/logs/by-execution/[executionId]/route.ts b/apps/sim/app/api/logs/by-execution/[executionId]/route.ts index 172a77506cc..bab53092456 100644 --- a/apps/sim/app/api/logs/by-execution/[executionId]/route.ts +++ b/apps/sim/app/api/logs/by-execution/[executionId]/route.ts @@ -2,7 +2,7 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { getLogByExecutionIdContract } from '@/lib/api/contracts/logs' import { parseRequest } from '@/lib/api/server' -import { getSession } from '@/lib/auth' +import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { fetchLogDetail } from '@/lib/logs/fetch-log-detail' @@ -10,9 +10,12 @@ const logger = createLogger('LogDetailsByExecutionAPI') export const GET = withRouteHandler( async (request: NextRequest, context: { params: Promise<{ executionId: string }> }) => { - const session = await getSession() - if (!session?.user?.id) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + const authResult = await checkSessionOrInternalAuth(request, { requireWorkflowId: false }) + if (!authResult.success || !authResult.userId) { + return NextResponse.json( + { error: authResult.error || 'Authentication required' }, + { status: 401 } + ) } const parsed = await parseRequest(getLogByExecutionIdContract, request, context) @@ -22,7 +25,7 @@ export const GET = withRouteHandler( const { workspaceId } = parsed.data.query const data = await fetchLogDetail({ - userId: session.user.id, + userId: authResult.userId, workspaceId, lookupColumn: 'executionId', lookupValue: executionId, diff --git a/apps/sim/app/api/webhooks/trigger/[path]/route.test.ts b/apps/sim/app/api/webhooks/trigger/[path]/route.test.ts index 83d3b81b14f..29850b5fe27 100644 --- a/apps/sim/app/api/webhooks/trigger/[path]/route.test.ts +++ b/apps/sim/app/api/webhooks/trigger/[path]/route.test.ts @@ -487,6 +487,49 @@ describe('Webhook Trigger API Route', () => { expect(text).toMatch(/not found/i) }) + describe('Internal trigger providers', () => { + it.each(['sim', 'table'])( + 'rejects HTTP deliveries to %s trigger paths with 404', + async (provider) => { + testData.webhooks.push({ + id: `${provider}-webhook-id`, + provider, + path: 'internal-path', + isActive: true, + providerConfig: { eventType: 'execution_error' }, + workflowId: 'test-workflow-id', + }) + + const req = createMockRequest('POST', { event: 'execution_error', forged: true }) + const params = Promise.resolve({ path: 'internal-path' }) + + const response = await POST(req as any, { params }) + + expect(response.status).toBe(404) + expect(queueWebhookExecutionMock).not.toHaveBeenCalled() + } + ) + + it('does not affect normal provider paths', async () => { + testData.webhooks.push({ + id: 'generic-webhook-id', + provider: 'generic', + path: 'normal-path', + isActive: true, + providerConfig: { requireAuth: false }, + workflowId: 'test-workflow-id', + }) + + const req = createMockRequest('POST', { event: 'test' }) + const params = Promise.resolve({ path: 'normal-path' }) + + const response = await POST(req as any, { params }) + + expect(response.status).toBe(200) + expect(queueWebhookExecutionMock).toHaveBeenCalledOnce() + }) + }) + describe('Generic Webhook Authentication', () => { it('passes correlation-bearing request context into webhook queueing', async () => { testData.webhooks.push({ diff --git a/apps/sim/app/api/webhooks/trigger/[path]/route.ts b/apps/sim/app/api/webhooks/trigger/[path]/route.ts index 73e71b1f3dc..166fddffcf8 100644 --- a/apps/sim/app/api/webhooks/trigger/[path]/route.ts +++ b/apps/sim/app/api/webhooks/trigger/[path]/route.ts @@ -18,6 +18,7 @@ import { verifyProviderAuth, } from '@/lib/webhooks/processor' import { blockExistsInDeployment } from '@/lib/workflows/persistence/utils' +import { isInternalTriggerProvider } from '@/triggers/constants' const logger = createLogger('WebhookTriggerAPI') @@ -89,7 +90,19 @@ async function handleWebhookPost( } // Find all webhooks for this path (supports credential set fan-out where multiple webhooks share a path) - const webhooksForPath = await findAllWebhooksForPath({ requestId, path }) + const allWebhooksForPath = await findAllWebhooksForPath({ requestId, path }) + + // Internal trigger providers (sim, table) are fired in-process, never over + // HTTP. Their rows still register a path, so reject deliveries here to keep + // forged events out. + const webhooksForPath = allWebhooksForPath.filter( + ({ webhook: foundWebhook }) => !isInternalTriggerProvider(foundWebhook.provider) + ) + + if (allWebhooksForPath.length > 0 && webhooksForPath.length === 0) { + logger.warn(`[${requestId}] Rejected HTTP delivery to internal trigger path: ${path}`) + return new NextResponse('Not Found', { status: 404 }) + } if (webhooksForPath.length === 0) { const verificationResponse = await handlePreLookupWebhookVerification( diff --git a/apps/sim/app/api/notifications/poll/route.test.ts b/apps/sim/app/api/workspace-events/poll/route.test.ts similarity index 69% rename from apps/sim/app/api/notifications/poll/route.test.ts rename to apps/sim/app/api/workspace-events/poll/route.test.ts index fd41807c47e..a577d54b4fb 100644 --- a/apps/sim/app/api/notifications/poll/route.test.ts +++ b/apps/sim/app/api/workspace-events/poll/route.test.ts @@ -1,14 +1,16 @@ /** - * Tests for the inactivity-alert polling cron route. + * Tests for the workspace-events no-activity polling cron route. * * @vitest-environment node */ import { createMockRequest, redisConfigMock, redisConfigMockFns } from '@sim/testing' import { beforeEach, describe, expect, it, vi } from 'vitest' -const { mockVerifyCronAuth, mockPollInactivityAlerts } = vi.hoisted(() => ({ +const { mockVerifyCronAuth, mockPollNoActivityEvents } = vi.hoisted(() => ({ mockVerifyCronAuth: vi.fn().mockReturnValue(null), - mockPollInactivityAlerts: vi.fn().mockResolvedValue({ checked: 0, delivered: 0 }), + mockPollNoActivityEvents: vi + .fn() + .mockResolvedValue({ subscriptions: 0, checked: 0, fired: 0, skipped: 0 }), })) vi.mock('@/lib/auth/internal', () => ({ @@ -17,25 +19,30 @@ vi.mock('@/lib/auth/internal', () => ({ vi.mock('@/lib/core/config/redis', () => redisConfigMock) -vi.mock('@/lib/notifications/inactivity-polling', () => ({ - pollInactivityAlerts: mockPollInactivityAlerts, +vi.mock('@/lib/workspace-events/no-activity', () => ({ + pollNoActivityEvents: mockPollNoActivityEvents, })) import { GET } from './route' function createRequest() { - return createMockRequest('GET', undefined, {}, 'http://localhost:3000/api/notifications/poll') + return createMockRequest('GET', undefined, {}, 'http://localhost:3000/api/workspace-events/poll') } const flushMicrotasks = () => new Promise((resolve) => setTimeout(resolve, 0)) -describe('inactivity alert polling route (fire-and-forget)', () => { +describe('workspace events polling route (fire-and-forget)', () => { beforeEach(() => { vi.clearAllMocks() redisConfigMockFns.mockAcquireLock.mockResolvedValue(true) redisConfigMockFns.mockReleaseLock.mockResolvedValue(true) mockVerifyCronAuth.mockReturnValue(null) - mockPollInactivityAlerts.mockResolvedValue({ checked: 0, delivered: 0 }) + mockPollNoActivityEvents.mockResolvedValue({ + subscriptions: 0, + checked: 0, + fired: 0, + skipped: 0, + }) }) it('returns the auth error when cron auth fails', async () => { @@ -44,7 +51,7 @@ describe('inactivity alert polling route (fire-and-forget)', () => { const response = await GET(createRequest()) expect(response.status).toBe(401) - expect(mockPollInactivityAlerts).not.toHaveBeenCalled() + expect(mockPollNoActivityEvents).not.toHaveBeenCalled() }) it('acknowledges with 202 and polls in the background after acquiring the lock', async () => { @@ -54,15 +61,15 @@ describe('inactivity alert polling route (fire-and-forget)', () => { const data = await response.json() expect(data).toMatchObject({ status: 'started' }) expect(redisConfigMockFns.mockAcquireLock).toHaveBeenCalledWith( - 'inactivity-alert-polling-lock', + 'workspace-events-no-activity-poll-lock', expect.any(String), expect.any(Number) ) await flushMicrotasks() - expect(mockPollInactivityAlerts).toHaveBeenCalledTimes(1) + expect(mockPollNoActivityEvents).toHaveBeenCalledTimes(1) expect(redisConfigMockFns.mockReleaseLock).toHaveBeenCalledWith( - 'inactivity-alert-polling-lock', + 'workspace-events-no-activity-poll-lock', expect.any(String) ) }) @@ -75,18 +82,18 @@ describe('inactivity alert polling route (fire-and-forget)', () => { expect(response.status).toBe(202) const data = await response.json() expect(data).toMatchObject({ status: 'skip' }) - expect(mockPollInactivityAlerts).not.toHaveBeenCalled() + expect(mockPollNoActivityEvents).not.toHaveBeenCalled() }) it('releases the lock even when polling throws', async () => { - mockPollInactivityAlerts.mockRejectedValueOnce(new Error('poll failed')) + mockPollNoActivityEvents.mockRejectedValueOnce(new Error('poll failed')) const response = await GET(createRequest()) expect(response.status).toBe(202) await flushMicrotasks() expect(redisConfigMockFns.mockReleaseLock).toHaveBeenCalledWith( - 'inactivity-alert-polling-lock', + 'workspace-events-no-activity-poll-lock', expect.any(String) ) }) diff --git a/apps/sim/app/api/notifications/poll/route.ts b/apps/sim/app/api/workspace-events/poll/route.ts similarity index 75% rename from apps/sim/app/api/notifications/poll/route.ts rename to apps/sim/app/api/workspace-events/poll/route.ts index d81707cb192..6408eb0bd64 100644 --- a/apps/sim/app/api/notifications/poll/route.ts +++ b/apps/sim/app/api/workspace-events/poll/route.ts @@ -8,25 +8,25 @@ import { verifyCronAuth } from '@/lib/auth/internal' import { acquireLock, releaseLock } from '@/lib/core/config/redis' import { runDetached } from '@/lib/core/utils/background' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' -import { pollInactivityAlerts } from '@/lib/notifications/inactivity-polling' +import { pollNoActivityEvents } from '@/lib/workspace-events/no-activity' -const logger = createLogger('InactivityAlertPoll') +const logger = createLogger('WorkspaceEventsPoll') export const maxDuration = 120 -const LOCK_KEY = 'inactivity-alert-polling-lock' +const LOCK_KEY = 'workspace-events-no-activity-poll-lock' const LOCK_TTL_SECONDS = 120 export const GET = withRouteHandler(async (request: NextRequest) => { const requestId = generateShortId() - logger.info(`Inactivity alert polling triggered (${requestId})`) + logger.info(`Workspace events no-activity polling triggered (${requestId})`) const queryValidation = noInputSchema.safeParse( Object.fromEntries(request.nextUrl.searchParams.entries()) ) if (!queryValidation.success) return validationErrorResponse(queryValidation.error) try { - const authError = verifyCronAuth(request, 'Inactivity alert polling') + const authError = verifyCronAuth(request, 'Workspace events polling') if (authError) { return authError } @@ -45,9 +45,9 @@ export const GET = withRouteHandler(async (request: NextRequest) => { ) } - runDetached('inactivity-alert-polling', async () => { + runDetached('workspace-events-no-activity-polling', async () => { try { - await pollInactivityAlerts() + await pollNoActivityEvents() } finally { await releaseLock(LOCK_KEY, requestId).catch(() => {}) } @@ -56,18 +56,18 @@ export const GET = withRouteHandler(async (request: NextRequest) => { return NextResponse.json( { success: true, - message: 'Inactivity alert polling started', + message: 'Workspace events polling started', requestId, status: 'started', }, { status: 202 } ) } catch (error) { - logger.error(`Error during inactivity alert polling (${requestId}):`, error) + logger.error(`Error during workspace events polling (${requestId}):`, error) return NextResponse.json( { success: false, - message: 'Inactivity alert polling failed', + message: 'Workspace events polling failed', error: getErrorMessage(error, 'Unknown error'), requestId, }, diff --git a/apps/sim/app/api/workspaces/[id]/notifications/[notificationId]/route.ts b/apps/sim/app/api/workspaces/[id]/notifications/[notificationId]/route.ts deleted file mode 100644 index 3de7e2f26c8..00000000000 --- a/apps/sim/app/api/workspaces/[id]/notifications/[notificationId]/route.ts +++ /dev/null @@ -1,287 +0,0 @@ -import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' -import { db } from '@sim/db' -import { workflow, workspaceNotificationSubscription } from '@sim/db/schema' -import { createLogger } from '@sim/logger' -import { and, eq, inArray } from 'drizzle-orm' -import { type NextRequest, NextResponse } from 'next/server' -import { updateNotificationServerContract } from '@/lib/api/contracts/notifications' -import { parseRequest, validationErrorResponse } from '@/lib/api/server' -import { getSession } from '@/lib/auth' -import { encryptSecret } from '@/lib/core/security/encryption' -import { withRouteHandler } from '@/lib/core/utils/with-route-handler' -import { captureServerEvent } from '@/lib/posthog/server' -import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils' - -const logger = createLogger('WorkspaceNotificationAPI') - -type RouteParams = { params: Promise<{ id: string; notificationId: string }> } - -async function checkWorkspaceWriteAccess( - userId: string, - workspaceId: string -): Promise<{ hasAccess: boolean; permission: string | null }> { - const permission = await getUserEntityPermissions(userId, 'workspace', workspaceId) - const hasAccess = permission === 'write' || permission === 'admin' - return { hasAccess, permission } -} - -async function getSubscription(notificationId: string, workspaceId: string) { - const [subscription] = await db - .select() - .from(workspaceNotificationSubscription) - .where( - and( - eq(workspaceNotificationSubscription.id, notificationId), - eq(workspaceNotificationSubscription.workspaceId, workspaceId) - ) - ) - .limit(1) - return subscription -} - -export const GET = withRouteHandler(async (request: NextRequest, { params }: RouteParams) => { - try { - const session = await getSession() - if (!session?.user?.id) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) - } - - const { id: workspaceId, notificationId } = await params - const permission = await getUserEntityPermissions(session.user.id, 'workspace', workspaceId) - - if (!permission) { - return NextResponse.json({ error: 'Workspace not found' }, { status: 404 }) - } - - const subscription = await getSubscription(notificationId, workspaceId) - - if (!subscription) { - return NextResponse.json({ error: 'Notification not found' }, { status: 404 }) - } - - return NextResponse.json({ - data: { - id: subscription.id, - notificationType: subscription.notificationType, - workflowIds: subscription.workflowIds, - allWorkflows: subscription.allWorkflows, - levelFilter: subscription.levelFilter, - triggerFilter: subscription.triggerFilter, - includeFinalOutput: subscription.includeFinalOutput, - includeTraceSpans: subscription.includeTraceSpans, - includeRateLimits: subscription.includeRateLimits, - includeUsageData: subscription.includeUsageData, - webhookConfig: subscription.webhookConfig, - emailRecipients: subscription.emailRecipients, - slackConfig: subscription.slackConfig, - alertConfig: subscription.alertConfig, - active: subscription.active, - createdAt: subscription.createdAt, - updatedAt: subscription.updatedAt, - }, - }) - } catch (error) { - logger.error('Error fetching notification', { error }) - return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) - } -}) - -export const PUT = withRouteHandler(async (request: NextRequest, context: RouteParams) => { - try { - const session = await getSession() - if (!session?.user?.id) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) - } - - const { id: workspaceId, notificationId } = await context.params - const { hasAccess } = await checkWorkspaceWriteAccess(session.user.id, workspaceId) - - if (!hasAccess) { - return NextResponse.json({ error: 'Insufficient permissions' }, { status: 403 }) - } - - const existingSubscription = await getSubscription(notificationId, workspaceId) - - if (!existingSubscription) { - return NextResponse.json({ error: 'Notification not found' }, { status: 404 }) - } - - const parsed = await parseRequest(updateNotificationServerContract, request, context, { - validationErrorResponse: (error) => validationErrorResponse(error, 'Invalid request'), - }) - if (!parsed.success) return parsed.response - const data = parsed.data.body - - if (data.workflowIds && data.workflowIds.length > 0) { - const workflowsInWorkspace = await db - .select({ id: workflow.id }) - .from(workflow) - .where(and(eq(workflow.workspaceId, workspaceId), inArray(workflow.id, data.workflowIds))) - - const validIds = new Set(workflowsInWorkspace.map((w) => w.id)) - const invalidIds = data.workflowIds.filter((id) => !validIds.has(id)) - - if (invalidIds.length > 0) { - return NextResponse.json( - { error: 'Some workflow IDs do not belong to this workspace', invalidIds }, - { status: 400 } - ) - } - } - - const updateData: Record = { updatedAt: new Date() } - - if (data.workflowIds !== undefined) updateData.workflowIds = data.workflowIds - if (data.allWorkflows !== undefined) updateData.allWorkflows = data.allWorkflows - if (data.levelFilter !== undefined) updateData.levelFilter = data.levelFilter - if (data.triggerFilter !== undefined) updateData.triggerFilter = data.triggerFilter - if (data.includeFinalOutput !== undefined) - updateData.includeFinalOutput = data.includeFinalOutput - if (data.includeTraceSpans !== undefined) updateData.includeTraceSpans = data.includeTraceSpans - if (data.includeRateLimits !== undefined) updateData.includeRateLimits = data.includeRateLimits - if (data.includeUsageData !== undefined) updateData.includeUsageData = data.includeUsageData - if (data.alertConfig !== undefined) updateData.alertConfig = data.alertConfig - if (data.emailRecipients !== undefined) updateData.emailRecipients = data.emailRecipients - if (data.slackConfig !== undefined) updateData.slackConfig = data.slackConfig - if (data.active !== undefined) updateData.active = data.active - - // Handle webhookConfig with secret encryption - if (data.webhookConfig !== undefined) { - let webhookConfig = data.webhookConfig - if (webhookConfig?.secret) { - const { encrypted } = await encryptSecret(webhookConfig.secret) - webhookConfig = { ...webhookConfig, secret: encrypted } - } - updateData.webhookConfig = webhookConfig - } - - const [subscription] = await db - .update(workspaceNotificationSubscription) - .set(updateData) - .where(eq(workspaceNotificationSubscription.id, notificationId)) - .returning() - - logger.info('Updated notification subscription', { - workspaceId, - subscriptionId: subscription.id, - }) - - recordAudit({ - workspaceId, - actorId: session.user.id, - action: AuditAction.NOTIFICATION_UPDATED, - resourceType: AuditResourceType.NOTIFICATION, - resourceId: notificationId, - resourceName: subscription.notificationType, - actorName: session.user.name ?? undefined, - actorEmail: session.user.email ?? undefined, - description: `Updated ${subscription.notificationType} notification subscription`, - metadata: { - notificationType: subscription.notificationType, - updatedFields: Object.keys(data).filter( - (k) => (data as Record)[k] !== undefined - ), - ...(data.active !== undefined && { active: data.active }), - ...(data.alertConfig !== undefined && { alertRule: data.alertConfig?.rule ?? null }), - }, - request, - }) - - return NextResponse.json({ - data: { - id: subscription.id, - notificationType: subscription.notificationType, - workflowIds: subscription.workflowIds, - allWorkflows: subscription.allWorkflows, - levelFilter: subscription.levelFilter, - triggerFilter: subscription.triggerFilter, - includeFinalOutput: subscription.includeFinalOutput, - includeTraceSpans: subscription.includeTraceSpans, - includeRateLimits: subscription.includeRateLimits, - includeUsageData: subscription.includeUsageData, - webhookConfig: subscription.webhookConfig, - emailRecipients: subscription.emailRecipients, - slackConfig: subscription.slackConfig, - alertConfig: subscription.alertConfig, - active: subscription.active, - createdAt: subscription.createdAt, - updatedAt: subscription.updatedAt, - }, - }) - } catch (error) { - logger.error('Error updating notification', { error }) - return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) - } -}) - -export const DELETE = withRouteHandler(async (request: NextRequest, { params }: RouteParams) => { - try { - const session = await getSession() - if (!session?.user?.id) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) - } - - const { id: workspaceId, notificationId } = await params - const { hasAccess } = await checkWorkspaceWriteAccess(session.user.id, workspaceId) - - if (!hasAccess) { - return NextResponse.json({ error: 'Insufficient permissions' }, { status: 403 }) - } - - const deleted = await db - .delete(workspaceNotificationSubscription) - .where( - and( - eq(workspaceNotificationSubscription.id, notificationId), - eq(workspaceNotificationSubscription.workspaceId, workspaceId) - ) - ) - .returning({ - id: workspaceNotificationSubscription.id, - notificationType: workspaceNotificationSubscription.notificationType, - }) - - if (deleted.length === 0) { - return NextResponse.json({ error: 'Notification not found' }, { status: 404 }) - } - - const deletedSubscription = deleted[0] - - logger.info('Deleted notification subscription', { - workspaceId, - subscriptionId: notificationId, - }) - - recordAudit({ - workspaceId, - actorId: session.user.id, - action: AuditAction.NOTIFICATION_DELETED, - resourceType: AuditResourceType.NOTIFICATION, - resourceId: notificationId, - actorName: session.user.name ?? undefined, - actorEmail: session.user.email ?? undefined, - resourceName: deletedSubscription.notificationType, - description: `Deleted ${deletedSubscription.notificationType} notification subscription`, - metadata: { - notificationType: deletedSubscription.notificationType, - }, - request, - }) - - captureServerEvent( - session.user.id, - 'notification_channel_deleted', - { - notification_id: notificationId, - notification_type: deletedSubscription.notificationType, - workspace_id: workspaceId, - }, - { groups: { workspace: workspaceId } } - ) - - return NextResponse.json({ success: true }) - } catch (error) { - logger.error('Error deleting notification', { error }) - return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) - } -}) diff --git a/apps/sim/app/api/workspaces/[id]/notifications/[notificationId]/test/route.ts b/apps/sim/app/api/workspaces/[id]/notifications/[notificationId]/test/route.ts deleted file mode 100644 index 97e6942808e..00000000000 --- a/apps/sim/app/api/workspaces/[id]/notifications/[notificationId]/test/route.ts +++ /dev/null @@ -1,348 +0,0 @@ -import { db } from '@sim/db' -import { account, workspaceNotificationSubscription } from '@sim/db/schema' -import { createLogger } from '@sim/logger' -import { hmacSha256Hex } from '@sim/security/hmac' -import { toError } from '@sim/utils/errors' -import { generateId } from '@sim/utils/id' -import { and, eq } from 'drizzle-orm' -import { type NextRequest, NextResponse } from 'next/server' -import { - type EmailRateLimitsData, - type EmailUsageData, - renderWorkflowNotificationEmail, -} from '@/components/emails' -import { notificationParamsSchema } from '@/lib/api/contracts/notifications' -import { getValidationErrorMessage } from '@/lib/api/server' -import { getSession } from '@/lib/auth' -import { decryptSecret } from '@/lib/core/security/encryption' -import { secureFetchWithValidation } from '@/lib/core/security/input-validation.server' -import { getBaseUrl } from '@/lib/core/utils/urls' -import { withRouteHandler } from '@/lib/core/utils/with-route-handler' -import { sendEmail } from '@/lib/messaging/email/mailer' -import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils' - -const logger = createLogger('WorkspaceNotificationTestAPI') - -type RouteParams = { params: Promise<{ id: string; notificationId: string }> } - -interface WebhookConfig { - url: string - secret?: string -} - -interface SlackConfig { - channelId: string - channelName: string - accountId: string -} - -function generateSignature(secret: string, timestamp: number, body: string): string { - const signatureBase = `${timestamp}.${body}` - return hmacSha256Hex(signatureBase, secret) -} - -function buildTestPayload(subscription: typeof workspaceNotificationSubscription.$inferSelect) { - const timestamp = Date.now() - const eventId = `evt_test_${generateId()}` - const executionId = `exec_test_${generateId()}` - - const payload: Record = { - id: eventId, - type: 'workflow.execution.completed', - timestamp, - data: { - workflowId: 'test-workflow-id', - workflowName: 'Test Workflow', - executionId, - status: 'success', - level: 'info', - trigger: 'manual', - startedAt: new Date(timestamp - 5000).toISOString(), - endedAt: new Date(timestamp).toISOString(), - totalDurationMs: 5000, - cost: { - total: 0.00123, - tokens: { input: 100, output: 50, total: 150 }, - }, - }, - links: { - log: `/workspace/logs`, - }, - } - - const data = payload.data as Record - - if (subscription.includeFinalOutput) { - data.finalOutput = { message: 'This is a test notification', test: true } - } - - if (subscription.includeRateLimits) { - data.rateLimits = { - sync: { - requestsPerMinute: 150, - remaining: 45, - resetAt: new Date(timestamp + 60000).toISOString(), - }, - async: { - requestsPerMinute: 1000, - remaining: 50, - resetAt: new Date(timestamp + 60000).toISOString(), - }, - } - } - - if (subscription.includeUsageData) { - data.usage = { currentPeriodCost: 2.45, limit: 20, percentUsed: 12.25, isExceeded: false } - } - - if (subscription.includeTraceSpans && subscription.notificationType === 'webhook') { - data.traceSpans = [ - { - name: 'test-block', - startTime: timestamp, - endTime: timestamp + 150, - duration: 150, - status: 'success', - blockId: 'block_test_1', - blockType: 'agent', - blockName: 'Test Agent', - children: [], - }, - ] - } - - return { payload, timestamp } -} - -async function testWebhook(subscription: typeof workspaceNotificationSubscription.$inferSelect) { - const webhookConfig = subscription.webhookConfig as WebhookConfig | null - if (!webhookConfig?.url) { - return { success: false, error: 'No webhook URL configured' } - } - - const { payload, timestamp } = buildTestPayload(subscription) - const body = JSON.stringify(payload) - const deliveryId = `delivery_test_${generateId()}` - - const headers: Record = { - 'Content-Type': 'application/json', - 'sim-event': 'workflow.execution.completed', - 'sim-timestamp': timestamp.toString(), - 'sim-delivery-id': deliveryId, - 'Idempotency-Key': deliveryId, - } - - if (webhookConfig.secret) { - const { decrypted } = await decryptSecret(webhookConfig.secret) - const signature = generateSignature(decrypted, timestamp, body) - headers['sim-signature'] = `t=${timestamp},v1=${signature}` - } - - try { - const response = await secureFetchWithValidation( - webhookConfig.url, - { - method: 'POST', - headers, - body, - timeout: 10000, - allowHttp: true, - }, - 'webhookUrl' - ) - const responseBody = await response.text().catch(() => '') - - return { - success: response.ok, - status: response.status, - statusText: response.statusText, - body: responseBody.slice(0, 500), - timestamp: new Date().toISOString(), - } - } catch (error: unknown) { - logger.warn('Webhook test failed', { - error: toError(error).message, - }) - return { success: false, error: 'Failed to deliver webhook' } - } -} - -async function testEmail(subscription: typeof workspaceNotificationSubscription.$inferSelect) { - if (!subscription.emailRecipients || subscription.emailRecipients.length === 0) { - return { success: false, error: 'No email recipients configured' } - } - - const { payload } = buildTestPayload(subscription) - const data = (payload as Record).data as Record - const baseUrl = getBaseUrl() - const logUrl = `${baseUrl}/workspace/${subscription.workspaceId}/logs` - - const html = await renderWorkflowNotificationEmail({ - workflowName: data.workflowName as string, - status: data.status as 'success' | 'error', - trigger: data.trigger as string, - duration: `${data.totalDurationMs}ms`, - cost: `$${(((data.cost as Record)?.total as number) || 0).toFixed(4)}`, - logUrl, - finalOutput: data.finalOutput, - rateLimits: data.rateLimits as EmailRateLimitsData | undefined, - usageData: data.usage as EmailUsageData | undefined, - }) - - const result = await sendEmail({ - to: subscription.emailRecipients, - subject: `[Test] Workflow Execution: ${data.workflowName}`, - html, - text: `This is a test notification from Sim.\n\nWorkflow: ${data.workflowName}\nStatus: ${data.status}\nDuration: ${data.totalDurationMs}ms\n\nView Log: ${logUrl}\n\nThis notification is configured for workspace notifications.`, - emailType: 'notifications', - }) - - return { - success: result.success, - message: result.message, - timestamp: new Date().toISOString(), - } -} - -async function testSlack( - subscription: typeof workspaceNotificationSubscription.$inferSelect, - userId: string -) { - const slackConfig = subscription.slackConfig as SlackConfig | null - if (!slackConfig?.channelId || !slackConfig?.accountId) { - return { success: false, error: 'No Slack channel or account configured' } - } - - const [slackAccount] = await db - .select({ accessToken: account.accessToken }) - .from(account) - .where(and(eq(account.id, slackConfig.accountId), eq(account.userId, userId))) - .limit(1) - - if (!slackAccount?.accessToken) { - return { success: false, error: 'Slack account not found or not connected' } - } - - const { payload } = buildTestPayload(subscription) - const data = (payload as Record).data as Record - - const slackPayload = { - channel: slackConfig.channelId, - blocks: [ - { - type: 'header', - text: { type: 'plain_text', text: '🧪 Test Notification', emoji: true }, - }, - { - type: 'section', - fields: [ - { type: 'mrkdwn', text: `*Workflow:*\n${data.workflowName}` }, - { type: 'mrkdwn', text: `*Status:*\n✅ ${data.status}` }, - { type: 'mrkdwn', text: `*Duration:*\n${data.totalDurationMs}ms` }, - { type: 'mrkdwn', text: `*Trigger:*\n${data.trigger}` }, - ], - }, - { - type: 'context', - elements: [ - { - type: 'mrkdwn', - text: 'This is a test notification from Sim workspace notifications.', - }, - ], - }, - ], - text: `Test notification: ${data.workflowName} - ${data.status}`, - } - - try { - const response = await fetch('https://slack.com/api/chat.postMessage', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${slackAccount.accessToken}`, - }, - body: JSON.stringify(slackPayload), - }) - - const result = await response.json() - - return { - success: result.ok, - error: result.ok ? undefined : `Slack error: ${result.error || 'unknown'}`, - channel: result.channel, - timestamp: new Date().toISOString(), - } - } catch (error: unknown) { - logger.warn('Slack test notification failed', { - error: toError(error).message, - }) - return { success: false, error: 'Failed to send Slack notification' } - } -} - -export const POST = withRouteHandler(async (request: NextRequest, { params }: RouteParams) => { - try { - const session = await getSession() - if (!session?.user?.id) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) - } - - const paramsResult = notificationParamsSchema.safeParse(await params) - if (!paramsResult.success) { - return NextResponse.json( - { error: getValidationErrorMessage(paramsResult.error, 'Invalid route parameters') }, - { status: 400 } - ) - } - const { id: workspaceId, notificationId } = paramsResult.data - const permission = await getUserEntityPermissions(session.user.id, 'workspace', workspaceId) - - if (permission !== 'write' && permission !== 'admin') { - return NextResponse.json({ error: 'Insufficient permissions' }, { status: 403 }) - } - - const [subscription] = await db - .select() - .from(workspaceNotificationSubscription) - .where( - and( - eq(workspaceNotificationSubscription.id, notificationId), - eq(workspaceNotificationSubscription.workspaceId, workspaceId) - ) - ) - .limit(1) - - if (!subscription) { - return NextResponse.json({ error: 'Notification not found' }, { status: 404 }) - } - - let result: Record - - switch (subscription.notificationType) { - case 'webhook': - result = await testWebhook(subscription) - break - case 'email': - result = await testEmail(subscription) - break - case 'slack': - result = await testSlack(subscription, session.user.id) - break - default: - return NextResponse.json({ error: 'Unknown notification type' }, { status: 400 }) - } - - logger.info('Test notification sent', { - workspaceId, - subscriptionId: notificationId, - type: subscription.notificationType, - success: result.success, - }) - - return NextResponse.json({ data: result }) - } catch (error) { - logger.error('Error testing notification', { error }) - return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) - } -}) diff --git a/apps/sim/app/api/workspaces/[id]/notifications/constants.ts b/apps/sim/app/api/workspaces/[id]/notifications/constants.ts deleted file mode 100644 index 036f32a5346..00000000000 --- a/apps/sim/app/api/workspaces/[id]/notifications/constants.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** Maximum email recipients per notification */ -export const MAX_EMAIL_RECIPIENTS = 10 - -/** Maximum notifications per type per workspace */ -export const MAX_NOTIFICATIONS_PER_TYPE = 10 - -/** Maximum workflow IDs per notification */ -export const MAX_WORKFLOW_IDS = 1000 diff --git a/apps/sim/app/api/workspaces/[id]/notifications/route.ts b/apps/sim/app/api/workspaces/[id]/notifications/route.ts deleted file mode 100644 index 9ddb0ed3fa8..00000000000 --- a/apps/sim/app/api/workspaces/[id]/notifications/route.ts +++ /dev/null @@ -1,227 +0,0 @@ -import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' -import { db } from '@sim/db' -import { workflow, workspaceNotificationSubscription } from '@sim/db/schema' -import { createLogger } from '@sim/logger' -import { generateId } from '@sim/utils/id' -import { and, eq, inArray } from 'drizzle-orm' -import { type NextRequest, NextResponse } from 'next/server' -import { createNotificationServerContract } from '@/lib/api/contracts/notifications' -import { parseRequest, validationErrorResponse } from '@/lib/api/server' -import { getSession } from '@/lib/auth' -import { encryptSecret } from '@/lib/core/security/encryption' -import { withRouteHandler } from '@/lib/core/utils/with-route-handler' -import { captureServerEvent } from '@/lib/posthog/server' -import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils' -import { MAX_NOTIFICATIONS_PER_TYPE } from './constants' - -const logger = createLogger('WorkspaceNotificationsAPI') - -async function checkWorkspaceWriteAccess( - userId: string, - workspaceId: string -): Promise<{ hasAccess: boolean; permission: string | null }> { - const permission = await getUserEntityPermissions(userId, 'workspace', workspaceId) - const hasAccess = permission === 'write' || permission === 'admin' - return { hasAccess, permission } -} - -export const GET = withRouteHandler( - async (request: NextRequest, { params }: { params: Promise<{ id: string }> }) => { - try { - const session = await getSession() - if (!session?.user?.id) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) - } - - const { id: workspaceId } = await params - const permission = await getUserEntityPermissions(session.user.id, 'workspace', workspaceId) - - if (!permission) { - return NextResponse.json({ error: 'Workspace not found' }, { status: 404 }) - } - - const subscriptions = await db - .select({ - id: workspaceNotificationSubscription.id, - notificationType: workspaceNotificationSubscription.notificationType, - workflowIds: workspaceNotificationSubscription.workflowIds, - allWorkflows: workspaceNotificationSubscription.allWorkflows, - levelFilter: workspaceNotificationSubscription.levelFilter, - triggerFilter: workspaceNotificationSubscription.triggerFilter, - includeFinalOutput: workspaceNotificationSubscription.includeFinalOutput, - includeTraceSpans: workspaceNotificationSubscription.includeTraceSpans, - includeRateLimits: workspaceNotificationSubscription.includeRateLimits, - includeUsageData: workspaceNotificationSubscription.includeUsageData, - webhookConfig: workspaceNotificationSubscription.webhookConfig, - emailRecipients: workspaceNotificationSubscription.emailRecipients, - slackConfig: workspaceNotificationSubscription.slackConfig, - alertConfig: workspaceNotificationSubscription.alertConfig, - active: workspaceNotificationSubscription.active, - createdAt: workspaceNotificationSubscription.createdAt, - updatedAt: workspaceNotificationSubscription.updatedAt, - }) - .from(workspaceNotificationSubscription) - .where(eq(workspaceNotificationSubscription.workspaceId, workspaceId)) - .orderBy(workspaceNotificationSubscription.createdAt) - - return NextResponse.json({ data: subscriptions }) - } catch (error) { - logger.error('Error fetching notifications', { error }) - return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) - } - } -) - -export const POST = withRouteHandler( - async (request: NextRequest, context: { params: Promise<{ id: string }> }) => { - try { - const session = await getSession() - if (!session?.user?.id) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) - } - - const { id: workspaceId } = await context.params - const { hasAccess } = await checkWorkspaceWriteAccess(session.user.id, workspaceId) - - if (!hasAccess) { - return NextResponse.json({ error: 'Insufficient permissions' }, { status: 403 }) - } - - const parsed = await parseRequest(createNotificationServerContract, request, context, { - validationErrorResponse: (error) => validationErrorResponse(error, 'Invalid request'), - }) - if (!parsed.success) return parsed.response - const data = parsed.data.body - - const existingCount = await db - .select({ id: workspaceNotificationSubscription.id }) - .from(workspaceNotificationSubscription) - .where( - and( - eq(workspaceNotificationSubscription.workspaceId, workspaceId), - eq(workspaceNotificationSubscription.notificationType, data.notificationType) - ) - ) - - if (existingCount.length >= MAX_NOTIFICATIONS_PER_TYPE) { - return NextResponse.json( - { - error: `Maximum ${MAX_NOTIFICATIONS_PER_TYPE} ${data.notificationType} notifications per workspace`, - }, - { status: 400 } - ) - } - - if (!data.allWorkflows && data.workflowIds.length > 0) { - const workflowsInWorkspace = await db - .select({ id: workflow.id }) - .from(workflow) - .where(and(eq(workflow.workspaceId, workspaceId), inArray(workflow.id, data.workflowIds))) - - const validIds = new Set(workflowsInWorkspace.map((w) => w.id)) - const invalidIds = data.workflowIds.filter((id) => !validIds.has(id)) - - if (invalidIds.length > 0) { - return NextResponse.json( - { error: 'Some workflow IDs do not belong to this workspace', invalidIds }, - { status: 400 } - ) - } - } - - let webhookConfig = data.webhookConfig || null - if (webhookConfig?.secret) { - const { encrypted } = await encryptSecret(webhookConfig.secret) - webhookConfig = { ...webhookConfig, secret: encrypted } - } - - const [subscription] = await db - .insert(workspaceNotificationSubscription) - .values({ - id: generateId(), - workspaceId, - notificationType: data.notificationType, - workflowIds: data.workflowIds, - allWorkflows: data.allWorkflows, - levelFilter: data.levelFilter, - triggerFilter: data.triggerFilter, - includeFinalOutput: data.includeFinalOutput, - includeTraceSpans: data.includeTraceSpans, - includeRateLimits: data.includeRateLimits, - includeUsageData: data.includeUsageData, - alertConfig: data.alertConfig || null, - webhookConfig, - emailRecipients: data.emailRecipients || null, - slackConfig: data.slackConfig || null, - createdBy: session.user.id, - }) - .returning() - - logger.info('Created notification subscription', { - workspaceId, - subscriptionId: subscription.id, - type: data.notificationType, - }) - - captureServerEvent( - session.user.id, - 'notification_channel_created', - { - workspace_id: workspaceId, - notification_type: data.notificationType, - alert_rule: data.alertConfig?.rule ?? null, - }, - { groups: { workspace: workspaceId } } - ) - - recordAudit({ - workspaceId, - actorId: session.user.id, - action: AuditAction.NOTIFICATION_CREATED, - resourceType: AuditResourceType.NOTIFICATION, - resourceId: subscription.id, - resourceName: data.notificationType, - actorName: session.user.name ?? undefined, - actorEmail: session.user.email ?? undefined, - description: `Created ${data.notificationType} notification subscription`, - metadata: { - notificationType: data.notificationType, - allWorkflows: data.allWorkflows, - workflowCount: data.workflowIds.length, - levelFilter: data.levelFilter, - alertRule: data.alertConfig?.rule ?? null, - ...(data.notificationType === 'email' && { - recipientCount: data.emailRecipients?.length ?? 0, - }), - ...(data.notificationType === 'slack' && { channelName: data.slackConfig?.channelName }), - }, - request, - }) - - return NextResponse.json({ - data: { - id: subscription.id, - notificationType: subscription.notificationType, - workflowIds: subscription.workflowIds, - allWorkflows: subscription.allWorkflows, - levelFilter: subscription.levelFilter, - triggerFilter: subscription.triggerFilter, - includeFinalOutput: subscription.includeFinalOutput, - includeTraceSpans: subscription.includeTraceSpans, - includeRateLimits: subscription.includeRateLimits, - includeUsageData: subscription.includeUsageData, - webhookConfig: subscription.webhookConfig, - emailRecipients: subscription.emailRecipients, - slackConfig: subscription.slackConfig, - alertConfig: subscription.alertConfig, - active: subscription.active, - createdAt: subscription.createdAt, - updatedAt: subscription.updatedAt, - }, - }) - } catch (error) { - logger.error('Error creating notification', { error }) - return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) - } - } -) diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/index.ts b/apps/sim/app/workspace/[workspaceId]/logs/components/index.ts index 4bb7d5cbd08..c8b8e357e15 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/index.ts @@ -4,4 +4,3 @@ export { ExecutionSnapshot } from './log-details/components/execution-snapshot' export { FileCards } from './log-details/components/file-download' export { TraceView } from './log-details/components/trace-view' export { LogRowContextMenu } from './log-row-context-menu' -export { NotificationSettings } from './logs-toolbar' diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/components/notifications/components/slack-channel-selector/index.ts b/apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/components/notifications/components/slack-channel-selector/index.ts deleted file mode 100644 index 502a98f5e65..00000000000 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/components/notifications/components/slack-channel-selector/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { SlackChannelSelector } from './slack-channel-selector' diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/components/notifications/components/slack-channel-selector/slack-channel-selector.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/components/notifications/components/slack-channel-selector/slack-channel-selector.tsx deleted file mode 100644 index 7010954417d..00000000000 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/components/notifications/components/slack-channel-selector/slack-channel-selector.tsx +++ /dev/null @@ -1,114 +0,0 @@ -'use client' - -import { useCallback, useEffect, useState } from 'react' -import { createLogger } from '@sim/logger' -import { getErrorMessage } from '@sim/utils/errors' -import { Hash, Lock } from 'lucide-react' -import { ChipCombobox, type ComboboxOption } from '@/components/emcn' -import { requestJson } from '@/lib/api/client/request' -import { slackChannelsSelectorContract } from '@/lib/api/contracts' - -const logger = createLogger('SlackChannelSelector') - -interface SlackChannel { - id: string - name: string - isPrivate: boolean -} - -interface SlackChannelSelectorProps { - accountId: string - value: string - onChange: (channelId: string, channelName: string) => void - disabled?: boolean - error?: string -} - -/** - * Standalone Slack channel selector that fetches channels for a given account. - */ -export function SlackChannelSelector({ - accountId, - value, - onChange, - disabled = false, - error, -}: SlackChannelSelectorProps) { - const [channels, setChannels] = useState([]) - const [isLoading, setIsLoading] = useState(false) - const [fetchError, setFetchError] = useState(null) - - const fetchChannels = useCallback(async () => { - if (!accountId) { - setChannels([]) - return - } - - setIsLoading(true) - setFetchError(null) - - try { - const data = await requestJson(slackChannelsSelectorContract, { - body: { credential: accountId }, - }) - setChannels( - (data.channels ?? []).map((channel) => ({ - id: channel.id, - name: channel.name, - isPrivate: channel.isPrivate, - })) - ) - } catch (err) { - logger.error('Failed to fetch Slack channels', { error: err }) - setFetchError(getErrorMessage(err, 'Failed to fetch channels')) - setChannels([]) - } finally { - setIsLoading(false) - } - }, [accountId]) - - useEffect(() => { - fetchChannels() - }, [fetchChannels]) - - const options: ComboboxOption[] = channels.map((channel) => ({ - label: channel.name, - value: channel.id, - icon: channel.isPrivate ? Lock : Hash, - })) - - const selectedChannel = channels.find((c) => c.id === value) - - const handleChange = (channelId: string) => { - const channel = channels.find((c) => c.id === channelId) - onChange(channelId, channel?.name || '') - } - - return ( -
- - {selectedChannel && !fetchError && ( -

- {selectedChannel.isPrivate ? 'Private' : 'Public'} channel: #{selectedChannel.name} -

- )} - {error &&

{error}

} -
- ) -} diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/components/notifications/components/workflow-selector/index.ts b/apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/components/notifications/components/workflow-selector/index.ts deleted file mode 100644 index 2055c3e6bd5..00000000000 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/components/notifications/components/workflow-selector/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { WorkflowSelector } from './workflow-selector' diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/components/notifications/components/workflow-selector/workflow-selector.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/components/notifications/components/workflow-selector/workflow-selector.tsx deleted file mode 100644 index 239f80572de..00000000000 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/components/notifications/components/workflow-selector/workflow-selector.tsx +++ /dev/null @@ -1,126 +0,0 @@ -'use client' - -import { useMemo } from 'react' -import { X } from 'lucide-react' -import { Badge, ChipCombobox, type ComboboxOption, Label, Skeleton } from '@/components/emcn' -import { useWorkflows } from '@/hooks/queries/workflows' - -interface WorkflowSelectorProps { - workspaceId: string - selectedIds: string[] - allWorkflows: boolean - onChange: (ids: string[], allWorkflows: boolean) => void - error?: string -} - -/** - * Multi-select workflow selector with "All Workflows" option. - * Uses Combobox's built-in showAllOption for the "All Workflows" selection. - * When allWorkflows is true, the array is empty and "All Workflows" is selected. - */ -export function WorkflowSelector({ - workspaceId, - selectedIds, - allWorkflows, - onChange, - error, -}: WorkflowSelectorProps) { - const { data: workflows = [], isPending: isLoading } = useWorkflows(workspaceId) - - const options: ComboboxOption[] = useMemo(() => { - return workflows.map((w) => ({ - label: w.name, - value: w.id, - })) - }, [workflows]) - - /** - * When allWorkflows is true, pass empty array so the "All" option is selected. - * Otherwise, pass the selected workflow IDs. - */ - const currentValues = allWorkflows ? [] : selectedIds - - /** - * Handle multi-select changes from Combobox. - * Empty array from showAllOption = all workflows selected. - */ - const handleMultiSelectChange = (values: string[]) => { - if (values.length === 0) { - onChange([], true) - } else { - onChange(values, false) - } - } - - const handleRemove = (e: React.MouseEvent, id: string) => { - e.preventDefault() - e.stopPropagation() - onChange( - selectedIds.filter((i) => i !== id), - false - ) - } - - const selectedWorkflows = useMemo(() => { - return workflows.filter((w) => selectedIds.includes(w.id)) - }, [workflows, selectedIds]) - - const overlayContent = useMemo(() => { - if (allWorkflows) { - return All Workflows - } - - if (selectedWorkflows.length === 0) { - return null - } - - return ( -
- {selectedWorkflows.slice(0, 2).map((w) => ( - handleRemove(e, w.id)} - > - {w.name} - - - ))} - {selectedWorkflows.length > 2 && ( - - +{selectedWorkflows.length - 2} - - )} -
- ) - }, [allWorkflows, selectedWorkflows, selectedIds]) - - if (isLoading) { - return ( -
- - -
- ) - } - - return ( -
- - -
- ) -} diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/components/notifications/index.ts b/apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/components/notifications/index.ts deleted file mode 100644 index 40536038c27..00000000000 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/components/notifications/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { NotificationSettings } from './notifications' diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/components/notifications/notifications.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/components/notifications/notifications.tsx deleted file mode 100644 index 4e65092dd69..00000000000 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/components/notifications/notifications.tsx +++ /dev/null @@ -1,1250 +0,0 @@ -'use client' - -import type { ReactNode } from 'react' -import { memo, useCallback, useEffect, useMemo, useState } from 'react' -import { createLogger } from '@sim/logger' -import { getErrorMessage } from '@sim/utils/errors' -import { X } from 'lucide-react' -import { - Badge, - Button, - ChipCombobox, - ChipConfirmModal, - ChipInput, - ChipModal, - ChipModalBody, - ChipModalField, - ChipModalFooter, - ChipModalHeader, - ChipModalTabs, - Skeleton, -} from '@/components/emcn' -import { SlackIcon } from '@/components/icons' -import type { - NotificationAlertRule, - NotificationLogLevel, - NotificationType, -} from '@/lib/api/contracts/notifications' -import { dollarsToCredits } from '@/lib/billing/credits/conversion' -import { getTriggerOptions } from '@/lib/logs/get-trigger-options' -import { - type NotificationSubscription, - useCreateNotification, - useDeleteNotification, - useNotifications, - useTestNotification, - useUpdateNotification, -} from '@/hooks/queries/notifications' -import { - useConnectedAccounts, - useConnectOAuthService, -} from '@/hooks/queries/oauth/oauth-connections' -import type { CoreTriggerType } from '@/stores/logs/filters/types' -import { SlackChannelSelector } from './components/slack-channel-selector' -import { WorkflowSelector } from './components/workflow-selector' - -const logger = createLogger('NotificationSettings') - -interface TabContentProps { - displayForm: boolean - renderForm: () => ReactNode - isLoading: boolean - filteredSubscriptions: NotificationSubscription[] - renderSubscriptionItem: (subscription: NotificationSubscription) => ReactNode -} - -function TabContent({ - displayForm, - renderForm, - isLoading, - filteredSubscriptions, - renderSubscriptionItem, -}: TabContentProps) { - if (displayForm) { - return renderForm() - } - - return ( -
-
- {isLoading ? ( -
- {[120, 80, 100, 90].map((labelWidth, i) => ( -
- - -
- ))} -
- ) : ( -
- {filteredSubscriptions.map(renderSubscriptionItem)} -
- )} -
-
- ) -} - -const TRIGGER_OPTIONS = getTriggerOptions() -const ALL_TRIGGER_VALUES = TRIGGER_OPTIONS.map((t) => t.value) - -type LogLevel = NotificationLogLevel -/** Contract alert rule plus a UI-only `'none'` sentinel meaning "no alert config". */ -type AlertRule = NotificationAlertRule | 'none' - -const ALERT_RULES: { value: AlertRule; label: string; description: string }[] = [ - { value: 'none', label: 'None', description: 'Notify on every matching execution' }, - { - value: 'consecutive_failures', - label: 'Consecutive Failures', - description: 'After X failures in a row', - }, - { value: 'failure_rate', label: 'Failure Rate', description: 'When failure % exceeds threshold' }, - { - value: 'latency_threshold', - label: 'Latency Threshold', - description: 'When execution exceeds duration', - }, - { value: 'latency_spike', label: 'Latency Spike', description: 'When slower than average by %' }, - { - value: 'cost_threshold', - label: 'Cost Threshold', - description: 'When execution cost exceeds credits', - }, - { value: 'no_activity', label: 'No Activity', description: 'When no executions in time window' }, - { value: 'error_count', label: 'Error Count', description: 'When errors exceed count in window' }, -] - -interface NotificationSettingsProps { - workspaceId: string - open: boolean - onOpenChange: (open: boolean) => void -} - -const LOG_LEVELS: LogLevel[] = ['info', 'error'] - -function formatAlertConfigLabel(config: { - rule: AlertRule - consecutiveFailures?: number - failureRatePercent?: number - windowHours?: number - durationThresholdMs?: number - latencySpikePercent?: number - costThresholdDollars?: number - inactivityHours?: number - errorCountThreshold?: number -}): string { - switch (config.rule) { - case 'consecutive_failures': - return `${config.consecutiveFailures} consecutive failures` - case 'failure_rate': - return `${config.failureRatePercent}% failure rate in ${config.windowHours}h` - case 'latency_threshold': - return `>${Math.round((config.durationThresholdMs || 0) / 1000)}s duration` - case 'latency_spike': - return `${config.latencySpikePercent}% above avg in ${config.windowHours}h` - case 'cost_threshold': - return `>${dollarsToCredits(config.costThresholdDollars ?? 0).toLocaleString()} credits per execution` - case 'no_activity': - return `No activity in ${config.inactivityHours}h` - case 'error_count': - return `${config.errorCountThreshold} errors in ${config.windowHours}h` - default: - return 'Alert rule' - } -} - -export const NotificationSettings = memo(function NotificationSettings({ - workspaceId, - open, - onOpenChange, -}: NotificationSettingsProps) { - const [activeTab, setActiveTab] = useState('webhook') - const [showForm, setShowForm] = useState(false) - const [editingId, setEditingId] = useState(null) - const [showDeleteDialog, setShowDeleteDialog] = useState(false) - const [deletingId, setDeletingId] = useState(null) - const [testStatus, setTestStatus] = useState<{ - id: string - success: boolean - message: string - } | null>(null) - - const [formData, setFormData] = useState({ - workflowIds: [] as string[], - allWorkflows: true, - levelFilter: ['info', 'error'] as LogLevel[], - triggerFilter: ALL_TRIGGER_VALUES, - includeFinalOutput: false, - includeTraceSpans: false, - includeRateLimits: false, - includeUsageData: false, - webhookUrl: '', - webhookSecret: '', - emailRecipients: [] as string[], - slackChannelId: '', - slackChannelName: '', - slackAccountId: '', - - alertRule: 'none' as AlertRule, - consecutiveFailures: 3, - failureRatePercent: 50, - windowHours: 24, - durationThresholdMs: 30000, - latencySpikePercent: 100, - costThresholdDollars: 1, - inactivityHours: 24, - errorCountThreshold: 10, - }) - - const [formErrors, setFormErrors] = useState>({}) - - const { data: subscriptions = [], isLoading } = useNotifications(open ? workspaceId : undefined) - const createNotification = useCreateNotification() - const updateNotification = useUpdateNotification() - const deleteNotification = useDeleteNotification() - const testNotification = useTestNotification() - - const { data: slackAccounts = [], isLoading: isLoadingSlackAccounts } = - useConnectedAccounts('slack') - const connectSlack = useConnectOAuthService() - - useEffect(() => { - if (testStatus) { - const timer = setTimeout(() => { - setTestStatus(null) - }, 2000) - return () => clearTimeout(timer) - } - }, [testStatus]) - - const filteredSubscriptions = useMemo(() => { - return subscriptions.filter((s) => s.notificationType === activeTab) - }, [subscriptions, activeTab]) - - const hasSubscriptions = filteredSubscriptions.length > 0 - - // Compute form visibility synchronously to avoid empty state flash - // Show form if user explicitly opened it OR if loading is complete with no subscriptions - const displayForm = showForm || (!isLoading && !hasSubscriptions && !editingId) - - const getSubscriptionsForTab = (tab: NotificationType) => { - return subscriptions.filter((s) => s.notificationType === tab) - } - - const resetForm = useCallback(() => { - setFormData({ - workflowIds: [], - allWorkflows: true, - levelFilter: ['info', 'error'], - triggerFilter: ALL_TRIGGER_VALUES, - includeFinalOutput: false, - includeTraceSpans: false, - includeRateLimits: false, - includeUsageData: false, - webhookUrl: '', - webhookSecret: '', - emailRecipients: [], - slackChannelId: '', - slackChannelName: '', - slackAccountId: '', - - alertRule: 'none', - consecutiveFailures: 3, - failureRatePercent: 50, - windowHours: 24, - durationThresholdMs: 30000, - latencySpikePercent: 100, - costThresholdDollars: 1, - inactivityHours: 24, - errorCountThreshold: 10, - }) - setFormErrors({}) - setEditingId(null) - }, []) - - const handleClose = useCallback(() => { - resetForm() - setShowForm(false) - setTestStatus(null) - onOpenChange(false) - }, [onOpenChange, resetForm]) - - const handleEmailRecipientsChange = useCallback((next: string[]) => { - setFormData((prev) => ({ ...prev, emailRecipients: next })) - setFormErrors((prev) => ({ ...prev, emailRecipients: '' })) - }, []) - - const validateForm = (): boolean => { - const errors: Record = {} - - if (!formData.allWorkflows && formData.workflowIds.length === 0) { - errors.workflows = 'Select at least one workflow or enable "All Workflows"' - } - - if (formData.levelFilter.length === 0) { - errors.levelFilter = 'Select at least one log level' - } - - if (formData.triggerFilter.length === 0) { - errors.triggerFilter = 'Select at least one trigger type' - } - - if (activeTab === 'webhook') { - if (!formData.webhookUrl) { - errors.webhookUrl = 'Webhook URL is required' - } else { - try { - const url = new URL(formData.webhookUrl) - if (!['http:', 'https:'].includes(url.protocol)) { - errors.webhookUrl = 'URL must start with http:// or https://' - } - } catch { - errors.webhookUrl = 'Invalid URL format' - } - } - } - - if (activeTab === 'email') { - if (formData.emailRecipients.length === 0) { - errors.emailRecipients = 'At least one email address is required' - } else if (formData.emailRecipients.length > 10) { - errors.emailRecipients = 'Maximum 10 email recipients allowed' - } - } - - if (activeTab === 'slack') { - if (!formData.slackAccountId) { - errors.slackAccountId = 'Select a Slack account' - } - if (!formData.slackChannelId) { - errors.slackChannelId = 'Select a Slack channel' - } - } - - if (formData.alertRule !== 'none') { - switch (formData.alertRule) { - case 'consecutive_failures': - if (formData.consecutiveFailures < 1 || formData.consecutiveFailures > 100) { - errors.consecutiveFailures = 'Must be between 1 and 100' - } - break - case 'failure_rate': - if (formData.failureRatePercent < 1 || formData.failureRatePercent > 100) { - errors.failureRatePercent = 'Must be between 1 and 100' - } - if (formData.windowHours < 1 || formData.windowHours > 168) { - errors.windowHours = 'Must be between 1 and 168 hours' - } - break - case 'latency_threshold': - if (formData.durationThresholdMs < 1000 || formData.durationThresholdMs > 3600000) { - errors.durationThresholdMs = 'Must be between 1s and 1 hour' - } - break - case 'latency_spike': - if (formData.latencySpikePercent < 10 || formData.latencySpikePercent > 1000) { - errors.latencySpikePercent = 'Must be between 10% and 1000%' - } - if (formData.windowHours < 1 || formData.windowHours > 168) { - errors.windowHours = 'Must be between 1 and 168 hours' - } - break - case 'cost_threshold': - if (formData.costThresholdDollars < 0.01 || formData.costThresholdDollars > 1000) { - errors.costThresholdDollars = 'Must be between $0.01 and $1000' - } - break - case 'no_activity': - if (formData.inactivityHours < 1 || formData.inactivityHours > 168) { - errors.inactivityHours = 'Must be between 1 and 168 hours' - } - break - case 'error_count': - if (formData.errorCountThreshold < 1 || formData.errorCountThreshold > 1000) { - errors.errorCountThreshold = 'Must be between 1 and 1000' - } - if (formData.windowHours < 1 || formData.windowHours > 168) { - errors.windowHours = 'Must be between 1 and 168 hours' - } - break - } - } - - setFormErrors(errors) - return Object.keys(errors).length === 0 - } - - const handleSave = async () => { - if (!validateForm()) return - - const alertConfig = - formData.alertRule !== 'none' - ? { - rule: formData.alertRule, - ...(formData.alertRule === 'consecutive_failures' && { - consecutiveFailures: formData.consecutiveFailures, - }), - ...(formData.alertRule === 'failure_rate' && { - failureRatePercent: formData.failureRatePercent, - windowHours: formData.windowHours, - }), - ...(formData.alertRule === 'latency_threshold' && { - durationThresholdMs: formData.durationThresholdMs, - }), - ...(formData.alertRule === 'latency_spike' && { - latencySpikePercent: formData.latencySpikePercent, - windowHours: formData.windowHours, - }), - ...(formData.alertRule === 'cost_threshold' && { - costThresholdDollars: formData.costThresholdDollars, - }), - ...(formData.alertRule === 'no_activity' && { - inactivityHours: formData.inactivityHours, - }), - ...(formData.alertRule === 'error_count' && { - errorCountThreshold: formData.errorCountThreshold, - windowHours: formData.windowHours, - }), - } - : null - - const payload = { - notificationType: activeTab, - workflowIds: formData.workflowIds, - allWorkflows: formData.allWorkflows, - levelFilter: formData.levelFilter, - triggerFilter: formData.triggerFilter as CoreTriggerType[], - includeFinalOutput: formData.includeFinalOutput, - // Trace spans only available for webhooks (too large for email/Slack) - includeTraceSpans: activeTab === 'webhook' ? formData.includeTraceSpans : false, - includeRateLimits: formData.includeRateLimits, - includeUsageData: formData.includeUsageData, - alertConfig, - ...(activeTab === 'webhook' && { - webhookConfig: { - url: formData.webhookUrl, - secret: formData.webhookSecret || undefined, - }, - }), - ...(activeTab === 'email' && { - emailRecipients: formData.emailRecipients, - }), - ...(activeTab === 'slack' && { - slackConfig: { - channelId: formData.slackChannelId, - channelName: formData.slackChannelName, - accountId: formData.slackAccountId, - }, - }), - } - - try { - if (editingId) { - await updateNotification.mutateAsync({ - workspaceId, - notificationId: editingId, - data: payload, - }) - } else { - await createNotification.mutateAsync({ - workspaceId, - data: payload, - }) - } - resetForm() - setShowForm(false) - } catch (error) { - const message = getErrorMessage(error, 'Failed to save notification') - setFormErrors({ general: message }) - } - } - - const handleBackToList = () => { - resetForm() - setShowForm(false) - } - - const handleAddNew = () => { - resetForm() - setShowForm(true) - } - - const handleEdit = (subscription: NotificationSubscription) => { - setActiveTab(subscription.notificationType) - setEditingId(subscription.id) - setFormData({ - workflowIds: subscription.workflowIds || [], - allWorkflows: subscription.allWorkflows, - levelFilter: subscription.levelFilter as LogLevel[], - triggerFilter: subscription.triggerFilter, - includeFinalOutput: subscription.includeFinalOutput, - includeTraceSpans: subscription.includeTraceSpans, - includeRateLimits: subscription.includeRateLimits, - includeUsageData: subscription.includeUsageData, - webhookUrl: subscription.webhookConfig?.url || '', - webhookSecret: '', - emailRecipients: subscription.emailRecipients || [], - slackChannelId: subscription.slackConfig?.channelId || '', - slackChannelName: subscription.slackConfig?.channelName || '', - slackAccountId: subscription.slackConfig?.accountId || '', - alertRule: subscription.alertConfig?.rule || 'none', - consecutiveFailures: subscription.alertConfig?.consecutiveFailures || 3, - failureRatePercent: subscription.alertConfig?.failureRatePercent || 50, - windowHours: subscription.alertConfig?.windowHours || 24, - durationThresholdMs: subscription.alertConfig?.durationThresholdMs || 30000, - latencySpikePercent: subscription.alertConfig?.latencySpikePercent || 100, - costThresholdDollars: subscription.alertConfig?.costThresholdDollars || 1, - inactivityHours: subscription.alertConfig?.inactivityHours || 24, - errorCountThreshold: subscription.alertConfig?.errorCountThreshold || 10, - }) - setShowForm(true) - } - - const handleDelete = async () => { - if (!deletingId) return - - try { - await deleteNotification.mutateAsync({ - workspaceId, - notificationId: deletingId, - }) - } catch (error) { - logger.error('Failed to delete notification', { error }) - } finally { - setShowDeleteDialog(false) - setDeletingId(null) - } - } - - const handleTest = async (id: string) => { - setTestStatus(null) - try { - const result = await testNotification.mutateAsync({ - workspaceId, - notificationId: id, - }) - setTestStatus({ - id, - success: result.data?.success ?? false, - message: - result.data?.error || (result.data?.success ? 'Test sent successfully' : 'Test failed'), - }) - } catch (_error) { - setTestStatus({ id, success: false, message: 'Failed to send test' }) - } - } - - const renderSubscriptionItem = (subscription: NotificationSubscription) => { - const identifier = - subscription.notificationType === 'webhook' - ? subscription.webhookConfig?.url - : subscription.notificationType === 'email' - ? subscription.emailRecipients?.join(', ') - : `#${subscription.slackConfig?.channelName || subscription.slackConfig?.channelId}` - - return ( -
-
-
-

- {identifier} -

-
- {subscription.allWorkflows ? ( - All workflows - ) : ( - - {subscription.workflowIds.length} workflow(s) - - )} - {subscription.levelFilter.map((level) => ( - - {level} - - ))} - {subscription.alertConfig && ( - - {formatAlertConfigLabel(subscription.alertConfig)} - - )} -
-
- -
- - - -
-
-
- ) - } - - const renderForm = () => ( -
-
- {formErrors.general && ( -

{formErrors.general}

- )} - -
- { - setFormData({ ...formData, workflowIds: ids, allWorkflows: all }) - setFormErrors({ ...formErrors, workflows: '' }) - }} - error={formErrors.workflows} - /> - - {activeTab === 'webhook' && ( - <> - { - setFormData({ ...formData, webhookUrl: value }) - setFormErrors({ ...formErrors, webhookUrl: '' }) - }} - error={formErrors.webhookUrl} - /> - setFormData({ ...formData, webhookSecret: value })} - /> - - )} - - {activeTab === 'email' && ( - - )} - - {activeTab === 'slack' && ( - <> - - {isLoadingSlackAccounts ? ( - - ) : slackAccounts.length === 0 ? ( -
- -
- ) : ( - ({ - value: acc.id, - label: acc.displayName || 'Slack Workspace', - }))} - value={formData.slackAccountId} - onChange={(value) => { - setFormData({ - ...formData, - slackAccountId: value, - slackChannelId: '', - }) - setFormErrors({ ...formErrors, slackAccountId: '', slackChannelId: '' }) - }} - placeholder='Select account...' - /> - )} -
- {slackAccounts.length > 0 && ( - - { - setFormData({ - ...formData, - slackChannelId: channelId, - slackChannelName: channelName, - }) - setFormErrors({ ...formErrors, slackChannelId: '' }) - }} - disabled={!formData.slackAccountId} - error={formErrors.slackChannelId} - /> - - )} - - )} - - - ({ - label: level.charAt(0).toUpperCase() + level.slice(1), - value: level, - }))} - multiSelect - multiSelectValues={formData.levelFilter} - onMultiSelectChange={(values) => { - setFormData({ ...formData, levelFilter: values as LogLevel[] }) - setFormErrors({ ...formErrors, levelFilter: '' }) - }} - placeholder='Select log levels...' - overlayContent={ - formData.levelFilter.length > 0 ? ( -
- {formData.levelFilter.map((level) => ( - { - e.preventDefault() - e.stopPropagation() - setFormData({ - ...formData, - levelFilter: formData.levelFilter.filter((l) => l !== level), - }) - }} - > - {level} - - - ))} -
- ) : null - } - showAllOption - allOptionLabel='All levels' - /> -
- - - ({ - label: t.label, - value: t.value, - }))} - multiSelect - multiSelectValues={formData.triggerFilter} - onMultiSelectChange={(values) => { - setFormData({ ...formData, triggerFilter: values }) - setFormErrors({ ...formErrors, triggerFilter: '' }) - }} - placeholder='Select trigger types...' - overlayContent={ - formData.triggerFilter.length > 0 ? ( -
- {formData.triggerFilter.slice(0, 6).map((trigger) => ( - { - e.preventDefault() - e.stopPropagation() - setFormData({ - ...formData, - triggerFilter: formData.triggerFilter.filter((t) => t !== trigger), - }) - }} - > - {trigger} - - - ))} - {formData.triggerFilter.length > 6 && ( - - +{formData.triggerFilter.length - 6} - - )} -
- ) : null - } - showAllOption - allOptionLabel='All triggers' - /> -
- - - { - setFormData({ - ...formData, - includeFinalOutput: values.includes('includeFinalOutput'), - includeTraceSpans: values.includes('includeTraceSpans'), - includeRateLimits: values.includes('includeRateLimits'), - includeUsageData: values.includes('includeUsageData'), - }) - }} - placeholder='Select data to include...' - overlayContent={(() => { - const labels: Record = { - includeFinalOutput: 'Final Output', - includeTraceSpans: 'Trace Spans', - includeRateLimits: 'Rate Limits', - includeUsageData: 'Usage Data', - } - const selected = [ - formData.includeFinalOutput && 'includeFinalOutput', - formData.includeTraceSpans && activeTab === 'webhook' && 'includeTraceSpans', - formData.includeRateLimits && 'includeRateLimits', - formData.includeUsageData && 'includeUsageData', - ].filter(Boolean) as string[] - - if (selected.length === 0) return null - - return ( -
- {selected.slice(0, 2).map((key) => ( - { - e.preventDefault() - e.stopPropagation() - setFormData({ ...formData, [key]: false }) - }} - > - {labels[key]} - - - ))} - {selected.length > 2 && ( - - +{selected.length - 2} - - )} -
- ) - })()} - showAllOption - allOptionLabel='None' - /> -
- - r.value === formData.alertRule)?.description} - > - ({ - value: rule.value, - label: rule.label, - }))} - value={formData.alertRule} - onChange={(value) => setFormData({ ...formData, alertRule: value as AlertRule })} - placeholder='Select rule' - /> - - - {formData.alertRule === 'consecutive_failures' && ( - - - setFormData({ - ...formData, - consecutiveFailures: Number.parseInt(e.target.value) || 1, - }) - } - /> - - )} - - {formData.alertRule === 'failure_rate' && ( -
- - - setFormData({ - ...formData, - failureRatePercent: Number.parseInt(e.target.value) || 1, - }) - } - /> - - - - setFormData({ - ...formData, - windowHours: Number.parseInt(e.target.value) || 1, - }) - } - /> - -
- )} - - {formData.alertRule === 'latency_threshold' && ( - - - setFormData({ - ...formData, - durationThresholdMs: (Number.parseInt(e.target.value) || 1) * 1000, - }) - } - /> - - )} - - {formData.alertRule === 'latency_spike' && ( -
- - - setFormData({ - ...formData, - latencySpikePercent: Number.parseInt(e.target.value) || 10, - }) - } - /> - - - - setFormData({ - ...formData, - windowHours: Number.parseInt(e.target.value) || 1, - }) - } - /> - -
- )} - - {formData.alertRule === 'cost_threshold' && ( - - - setFormData({ - ...formData, - costThresholdDollars: Number.parseFloat(e.target.value) || 0.01, - }) - } - /> - - )} - - {formData.alertRule === 'no_activity' && ( - - - setFormData({ - ...formData, - inactivityHours: Number.parseInt(e.target.value) || 1, - }) - } - /> - - )} - - {formData.alertRule === 'error_count' && ( -
- - - setFormData({ - ...formData, - errorCountThreshold: Number.parseInt(e.target.value) || 1, - }) - } - /> - - - - setFormData({ - ...formData, - windowHours: Number.parseInt(e.target.value) || 1, - }) - } - /> - -
- )} -
-
-
- ) - - return ( - <> - - handleClose()}>Notifications - - - { - const tab = value as NotificationType - const tabHasSubscriptions = getSubscriptionsForTab(tab).length > 0 - resetForm() - setActiveTab(tab) - setShowForm(!tabHasSubscriptions) - }} - /> - -
- -
-
- - -
- - { - if (!next) setDeletingId(null) - setShowDeleteDialog(next) - }} - srTitle='Delete Notification' - title='Delete Notification' - description={ - <> - - This will permanently remove the notification and stop all deliveries. - {' '} - This action cannot be undone. - - } - confirm={{ - label: 'Delete', - onClick: handleDelete, - pending: deleteNotification.isPending, - pendingLabel: 'Deleting...', - }} - /> - - ) -}) diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/index.ts b/apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/index.ts index 2884924bb2e..e96ac6652c5 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/index.ts @@ -1,3 +1,2 @@ -export { NotificationSettings } from './components/notifications' export { AutocompleteSearch } from './components/search' export { LogsToolbar } from './logs-toolbar' diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/logs-toolbar.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/logs-toolbar.tsx index d16921f748c..f628b636b9c 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/logs-toolbar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/logs-toolbar.tsx @@ -1,7 +1,7 @@ 'use client' import { memo, useCallback, useMemo, useRef, useState } from 'react' -import { ArrowUp, Bell, Library, MoreHorizontal, RefreshCw } from 'lucide-react' +import { ArrowUp, Library, MoreHorizontal, RefreshCw } from 'lucide-react' import { useParams } from 'next/navigation' import { usePostHog } from 'posthog-js/react' import { useShallow } from 'zustand/react/shallow' @@ -70,8 +70,6 @@ interface LogsToolbarProps { canEdit: boolean /** Whether there are logs to export */ hasLogs: boolean - /** Callback when notification settings is clicked */ - onOpenNotificationSettings: () => void /** Search query value */ searchQuery: string /** Callback when search query changes */ @@ -157,7 +155,6 @@ export const LogsToolbar = memo(function LogsToolbar({ onExport, canEdit, hasLogs, - onOpenNotificationSettings, searchQuery, onSearchQueryChange, onSearchOpenChange, @@ -440,10 +437,6 @@ export const LogsToolbar = memo(function LogsToolbar({ Export as CSV - - - Configure Notifications - diff --git a/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx b/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx index 1e6d70d4d0a..90967d51ee2 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx @@ -14,7 +14,6 @@ import { useQueryClient } from '@tanstack/react-query' import { useParams } from 'next/navigation' import { useShallow } from 'zustand/react/shallow' import { - Bell, Button, ChipCombobox, type ComboboxOption, @@ -74,13 +73,7 @@ import { useWorkflowMap, useWorkflows } from '@/hooks/queries/workflows' import { useDebounce } from '@/hooks/use-debounce' import { useFilterStore } from '@/stores/logs/filters/store' import { CORE_TRIGGER_TYPES } from '@/stores/logs/filters/types' -import { - Dashboard, - ExecutionSnapshot, - LogDetails, - LogRowContextMenu, - NotificationSettings, -} from './components' +import { Dashboard, ExecutionSnapshot, LogDetails, LogRowContextMenu } from './components' import { DELETED_WORKFLOW_LABEL, extractRetryInput, @@ -290,7 +283,6 @@ export default function Logs() { const activeLogRefetchRef = useRef<() => void>(() => {}) const activeLogTabRef = useRef('overview') const logsQueryRef = useRef({ isFetching: false, hasNextPage: false, fetchNextPage: () => {} }) - const [isNotificationSettingsOpen, setIsNotificationSettingsOpen] = useState(false) const [activeSort, setActiveSort] = useState<{ column: string direction: 'asc' | 'desc' @@ -720,7 +712,6 @@ export default function Logs() { }, []) const handleCloseContextMenu = useCallback(() => setContextMenuOpen(false), []) - const handleOpenNotificationSettings = useCallback(() => setIsNotificationSettingsOpen(true), []) function handleClosePreview() { setPreviewLogId(null) } @@ -1079,11 +1070,6 @@ export default function Logs() { onSelect: handleExport, disabled: !userPermissions.canEdit || isExporting || logs.length === 0, }, - { - text: 'Notifications', - icon: Bell, - onSelect: handleOpenNotificationSettings, - }, { text: 'Refresh', icon: refreshIcon, @@ -1111,7 +1097,6 @@ export default function Logs() { userPermissions.canEdit, isExporting, logs.length, - handleOpenNotificationSettings, ] ) @@ -1158,12 +1143,6 @@ export default function Logs() { )} - - { if (!multiSelect || !multiValues || multiValues.length === 0) return undefined + const visibleValues = multiValues.slice(0, MAX_VISIBLE_MULTI_SELECT_BADGES) + const overflowCount = multiValues.length - visibleValues.length + return (
- {multiValues.map((selectedValue: string, index) => { + {visibleValues.map((selectedValue: string, index) => { const label = (optionMap.get(selectedValue) || selectedValue).toLowerCase() const workflowSearchHighlight = getWorkflowSearchLabelHighlight({ activeSearchTarget, @@ -474,14 +481,18 @@ export const Dropdown = memo(function Dropdown({ label, }) return ( - - {formatDisplayText(label, { workflowSearchHighlight })} - + + + {formatDisplayText(label, { workflowSearchHighlight })} + + ) })} + {overflowCount > 0 && ( + + +{overflowCount} + + )}
) }, [activeSearchTarget, blockId, multiSelect, multiValues, optionMap, subBlockId]) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/toolbar/toolbar.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/toolbar/toolbar.tsx index 769c75c678e..174e5b43e3d 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/toolbar/toolbar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/toolbar/toolbar.tsx @@ -729,7 +729,7 @@ export const Toolbar = memo( onContextMenu={handleItemContextMenu} /> -/** - * Type guard for workflow table row structure (sub-block table inputs) - */ -interface WorkflowTableRow { - id: string - cells: Record -} - -/** - * Type guard for field format structure (input format, response format) - */ -interface FieldFormat { - id: string - name: string - type?: string - value?: string - collapsed?: boolean -} - -/** - * Checks if a value is a table row array - */ -const isTableRowArray = (value: unknown): value is WorkflowTableRow[] => { - if (!Array.isArray(value) || value.length === 0) return false - const firstItem = value[0] - return ( - typeof firstItem === 'object' && - firstItem !== null && - 'id' in firstItem && - 'cells' in firstItem && - typeof firstItem.cells === 'object' - ) -} - -/** - * Checks if a value is a field format array - */ -const isFieldFormatArray = (value: unknown): value is FieldFormat[] => { - if (!Array.isArray(value) || value.length === 0) return false - const firstItem = value[0] - return ( - typeof firstItem === 'object' && - firstItem !== null && - 'id' in firstItem && - 'name' in firstItem && - typeof firstItem.name === 'string' - ) -} - -/** - * Checks if a value is a plain object (not array, not null) - */ -const isPlainObject = (value: unknown): value is Record => { - return typeof value === 'object' && value !== null && !Array.isArray(value) -} - -/** - * Type guard for variable assignments array - */ -const isVariableAssignmentsArray = ( - value: unknown -): value is Array<{ id?: string; variableId?: string; variableName?: string; value: any }> => { - return ( - Array.isArray(value) && - value.length > 0 && - value.every( - (item) => - typeof item === 'object' && - item !== null && - ('variableName' in item || 'variableId' in item) - ) - ) -} - -/** - * Type guard for agent messages array - */ -const isMessagesArray = (value: unknown): value is Array<{ role: string; content: string }> => { - return ( - Array.isArray(value) && - value.length > 0 && - value.every( - (item) => - typeof item === 'object' && - item !== null && - 'role' in item && - 'content' in item && - typeof item.role === 'string' && - typeof item.content === 'string' - ) - ) -} - -/** - * Type guard for tag filter array (used in knowledge block filters) - */ -interface TagFilterItem { - id: string - tagName: string - fieldType?: string - operator?: string - tagValue: string -} - -const isTagFilterArray = (value: unknown): value is TagFilterItem[] => { - if (!Array.isArray(value) || value.length === 0) return false - const firstItem = value[0] - return ( - typeof firstItem === 'object' && - firstItem !== null && - 'tagName' in firstItem && - 'tagValue' in firstItem && - typeof firstItem.tagName === 'string' - ) -} - -/** - * Type guard for document tag entry array (used in knowledge block create document) - */ -interface DocumentTagItem { - id: string - tagName: string - fieldType?: string - value: string -} - -const isDocumentTagArray = (value: unknown): value is DocumentTagItem[] => { - if (!Array.isArray(value) || value.length === 0) return false - const firstItem = value[0] - return ( - typeof firstItem === 'object' && - firstItem !== null && - 'tagName' in firstItem && - 'value' in firstItem && - !('tagValue' in firstItem) && // Distinguish from tag filters - typeof firstItem.tagName === 'string' - ) -} - -/** - * Type guard for filter condition array (used in table block filter builder) - */ -const isFilterConditionArray = (value: unknown): value is FilterRule[] => { - if (!Array.isArray(value) || value.length === 0) return false - const firstItem = value[0] - return ( - typeof firstItem === 'object' && - firstItem !== null && - 'column' in firstItem && - 'operator' in firstItem && - 'logicalOperator' in firstItem && - typeof firstItem.column === 'string' - ) -} - -/** - * Type guard for sort condition array (used in table block sort builder) - */ -const isSortConditionArray = (value: unknown): value is SortRule[] => { - if (!Array.isArray(value) || value.length === 0) return false - const firstItem = value[0] - return ( - typeof firstItem === 'object' && - firstItem !== null && - 'column' in firstItem && - 'direction' in firstItem && - typeof firstItem.column === 'string' && - (firstItem.direction === 'asc' || firstItem.direction === 'desc') - ) -} - -/** - * Attempts to parse a JSON string, returns the parsed value or the original value if parsing fails - */ -const tryParseJson = (value: unknown): unknown => { - if (typeof value !== 'string') return value - try { - const trimmed = value.trim() - if ( - (trimmed.startsWith('[') && trimmed.endsWith(']')) || - (trimmed.startsWith('{') && trimmed.endsWith('}')) - ) { - return JSON.parse(trimmed) - } - } catch { - // Not valid JSON, return original - } - return value -} - -/** - * Formats a subblock value for display, intelligently handling nested objects and arrays. - * Used by both the canvas workflow blocks and copilot edit summaries. - */ -export const getDisplayValue = (value: unknown): string => { - if (value == null || value === '') return '-' - - const parsedValue = tryParseJson(value) - - if (isMessagesArray(parsedValue)) { - const firstMessage = parsedValue[0] - if (!firstMessage?.content || firstMessage.content.trim() === '') return '-' - const content = firstMessage.content.trim() - return truncate(content, 50) - } - - if (isVariableAssignmentsArray(parsedValue)) { - const names = parsedValue.map((a) => a.variableName).filter((name): name is string => !!name) - if (names.length === 0) return '-' - if (names.length === 1) return names[0] - if (names.length === 2) return `${names[0]}, ${names[1]}` - return `${names[0]}, ${names[1]} +${names.length - 2}` - } - - if (isTagFilterArray(parsedValue)) { - const validFilters = parsedValue.filter( - (f) => typeof f.tagName === 'string' && f.tagName.trim() !== '' - ) - if (validFilters.length === 0) return '-' - if (validFilters.length === 1) return validFilters[0].tagName - if (validFilters.length === 2) return `${validFilters[0].tagName}, ${validFilters[1].tagName}` - return `${validFilters[0].tagName}, ${validFilters[1].tagName} +${validFilters.length - 2}` - } - - if (isDocumentTagArray(parsedValue)) { - const validTags = parsedValue.filter( - (t) => typeof t.tagName === 'string' && t.tagName.trim() !== '' - ) - if (validTags.length === 0) return '-' - if (validTags.length === 1) return validTags[0].tagName - if (validTags.length === 2) return `${validTags[0].tagName}, ${validTags[1].tagName}` - return `${validTags[0].tagName}, ${validTags[1].tagName} +${validTags.length - 2}` - } - - if (isFilterConditionArray(parsedValue)) { - const validConditions = parsedValue.filter( - (c) => typeof c.column === 'string' && c.column.trim() !== '' - ) - if (validConditions.length === 0) return '-' - const formatCondition = (c: FilterRule) => { - const opLabels: Record = { - eq: '=', - ne: '≠', - gt: '>', - gte: '≥', - lt: '<', - lte: '≤', - contains: '~', - in: 'in', - } - const op = opLabels[c.operator] || c.operator - return `${c.column} ${op} ${c.value || '?'}` - } - if (validConditions.length === 1) return formatCondition(validConditions[0]) - if (validConditions.length === 2) { - return `${formatCondition(validConditions[0])}, ${formatCondition(validConditions[1])}` - } - return `${formatCondition(validConditions[0])}, ${formatCondition(validConditions[1])} +${validConditions.length - 2}` - } - - if (isSortConditionArray(parsedValue)) { - const validConditions = parsedValue.filter( - (c) => typeof c.column === 'string' && c.column.trim() !== '' - ) - if (validConditions.length === 0) return '-' - const formatSort = (c: SortRule) => `${c.column} ${c.direction === 'desc' ? '↓' : '↑'}` - if (validConditions.length === 1) return formatSort(validConditions[0]) - if (validConditions.length === 2) { - return `${formatSort(validConditions[0])}, ${formatSort(validConditions[1])}` - } - return `${formatSort(validConditions[0])}, ${formatSort(validConditions[1])} +${validConditions.length - 2}` - } - - if (isTableRowArray(parsedValue)) { - const nonEmptyRows = parsedValue.filter((row) => { - const cellValues = Object.values(row.cells) - return cellValues.some((cell) => cell && cell.trim() !== '') - }) - - if (nonEmptyRows.length === 0) return '-' - if (nonEmptyRows.length === 1) { - const firstRow = nonEmptyRows[0] - const cellEntries = Object.entries(firstRow.cells).filter(([, val]) => val?.trim()) - if (cellEntries.length === 0) return '-' - const preview = cellEntries - .slice(0, 2) - .map(([key, val]) => `${key}: ${val}`) - .join(', ') - return cellEntries.length > 2 ? `${preview}...` : preview - } - return `${nonEmptyRows.length} rows` - } - - if (isFieldFormatArray(parsedValue)) { - const namedFields = parsedValue.filter( - (field) => typeof field.name === 'string' && field.name.trim() !== '' - ) - if (namedFields.length === 0) return '-' - if (namedFields.length === 1) return namedFields[0].name - if (namedFields.length === 2) return `${namedFields[0].name}, ${namedFields[1].name}` - return `${namedFields[0].name}, ${namedFields[1].name} +${namedFields.length - 2}` - } - - if (isPlainObject(parsedValue)) { - const entries = Object.entries(parsedValue).filter( - ([, val]) => val !== null && val !== undefined && val !== '' - ) - - if (entries.length === 0) return '-' - if (entries.length === 1) { - const [key, val] = entries[0] - const valStr = String(val).slice(0, 30) - return `${key}: ${valStr}${String(val).length > 30 ? '...' : ''}` - } - const preview = entries - .slice(0, 2) - .map(([key]) => key) - .join(', ') - return entries.length > 2 ? `${preview} +${entries.length - 2}` : preview - } - - if (Array.isArray(parsedValue)) { - const nonEmptyItems = parsedValue.filter( - (item) => item !== null && item !== undefined && item !== '' - ) - if (nonEmptyItems.length === 0) return '-' - - const getItemDisplayValue = (item: unknown): string => { - if (typeof item === 'object' && item !== null) { - const obj = item as Record - return String(obj.title || obj.name || obj.label || obj.id || JSON.stringify(item)) - } - return String(item) - } - - if (nonEmptyItems.length === 1) return getItemDisplayValue(nonEmptyItems[0]) - if (nonEmptyItems.length === 2) { - return `${getItemDisplayValue(nonEmptyItems[0])}, ${getItemDisplayValue(nonEmptyItems[1])}` - } - return `${getItemDisplayValue(nonEmptyItems[0])}, ${getItemDisplayValue(nonEmptyItems[1])} +${nonEmptyItems.length - 2}` - } - - // For non-array, non-object values, use original value for string conversion - const stringValue = String(value) - if (stringValue === '[object Object]') { - try { - const json = JSON.stringify(parsedValue) - if (json.length <= 40) return json - return `${json.slice(0, 37)}...` - } catch { - return '-' - } - } - - return stringValue.trim().length > 0 ? stringValue : '-' -} - interface SubBlockRowProps { title: string value?: string @@ -540,20 +190,10 @@ const SubBlockRow = memo(function SubBlockRow({ const knowledgeBaseId = dependencyValues.knowledgeBaseId - const dropdownLabel = useMemo(() => { - if (!subBlock || (subBlock.type !== 'dropdown' && subBlock.type !== 'combobox')) return null - if (!rawValue || typeof rawValue !== 'string') return null - - const options = typeof subBlock.options === 'function' ? subBlock.options() : subBlock.options - if (!options) return null - - const option = options.find((opt) => - typeof opt === 'string' ? opt === rawValue : opt.id === rawValue - ) - - if (!option) return null - return typeof option === 'string' ? option : option.label - }, [subBlock, rawValue]) + const dropdownLabel = useMemo( + () => resolveDropdownLabel(subBlock, rawValue), + [subBlock, rawValue] + ) const resolveContextValue = useCallback( (key: string): string | undefined => { @@ -605,11 +245,26 @@ const SubBlockRow = memo(function SubBlockRow({ ) const knowledgeBaseDisplayName = kbForDisplayName?.name ?? null - const { data: workflowMapForLookup = {} } = useWorkflowMap(workspaceId) + const { + data: workflowMapForLookup = {}, + isSuccess: workflowMapLoaded, + isPlaceholderData: workflowMapIsPlaceholder, + } = useWorkflowMap(workspaceId) + /** + * Hydrates workflow-selector values and multi-select workflow dropdowns to + * names. Ready only on a successful, non-placeholder load — an errored or + * stale-placeholder map must not mislabel valid workflows as deleted. + */ const workflowSelectionName = useMemo(() => { - if (subBlock?.type !== 'workflow-selector' || typeof rawValue !== 'string') return null - return workflowMapForLookup[rawValue]?.name ?? null - }, [workflowMapForLookup, subBlock?.type, rawValue]) + const lookup = { + workflowMap: workflowMapForLookup, + ready: workflowMapLoaded && !workflowMapIsPlaceholder, + } + return ( + resolveWorkflowSelectionLabel(subBlock, rawValue, lookup) ?? + resolveWorkflowMultiSelectLabel(subBlock, rawValue, lookup) + ) + }, [workflowMapForLookup, workflowMapLoaded, workflowMapIsPlaceholder, subBlock, rawValue]) const { data: mcpServers = [] } = useMcpServers(workspaceId || '') const mcpServerDisplayName = useMemo(() => { @@ -671,145 +326,29 @@ const SubBlockRow = memo(function SubBlockRow({ isEqual ) - const variablesDisplayValue = useMemo(() => { - if (subBlock?.type !== 'variables-input' || !isVariableAssignmentsArray(rawValue)) { - return null - } - - const variablesArray = Object.values(workflowVariables) - - const names = rawValue - .map((a) => { - if (a.variableId) { - const variable = variablesArray.find((v: any) => v.id === a.variableId) - return variable?.name - } - if (a.variableName) return a.variableName - return null - }) - .filter((name): name is string => !!name) - - if (names.length === 0) return null - if (names.length === 1) return names[0] - if (names.length === 2) return `${names[0]}, ${names[1]}` - return `${names[0]}, ${names[1]} +${names.length - 2}` - }, [subBlock?.type, rawValue, workflowVariables]) + const variablesDisplayValue = useMemo( + () => resolveVariablesLabel(subBlock, rawValue, Object.values(workflowVariables)), + [subBlock, rawValue, workflowVariables] + ) - /** - * Hydrates tool references to display names. - * Follows the same pattern as other selectors (Slack channels, MCP tools, etc.) - */ + /** Hydrates tool references to display names. */ const { data: customTools = [] } = useCustomTools(workspaceId || '') + const toolsDisplayValue = useMemo( + () => resolveToolsLabel(subBlock, rawValue, customTools), + [subBlock, rawValue, customTools] + ) - const toolsDisplayValue = useMemo(() => { - if (subBlock?.type !== 'tool-input' || !Array.isArray(rawValue) || rawValue.length === 0) { - return null - } - - const toolNames = rawValue - .map((tool: any) => { - if (!tool || typeof tool !== 'object') return null - - // Priority 1: Use tool.title if already populated - if (tool.title && typeof tool.title === 'string') return tool.title - - // Priority 2: Resolve custom tools with reference ID from database - if (tool.type === 'custom-tool' && tool.customToolId) { - const customTool = customTools.find((t) => t.id === tool.customToolId) - if (customTool?.title) return customTool.title - if (customTool?.schema?.function?.name) return customTool.schema.function.name - } - - // Priority 3: Extract from inline schema (legacy format) - if (tool.schema?.function?.name) return tool.schema.function.name - - // Priority 4: Extract from OpenAI function format - if (tool.function?.name) return tool.function.name - - // Priority 5: Resolve built-in tool blocks from registry - if ( - typeof tool.type === 'string' && - tool.type !== 'custom-tool' && - tool.type !== 'mcp' && - tool.type !== 'workflow' && - tool.type !== 'workflow_input' - ) { - const blockConfig = getBlock(tool.type) - if (blockConfig?.name) return blockConfig.name - } - - return null - }) - .filter((name): name is string => !!name) - - if (toolNames.length === 0) return null - if (toolNames.length === 1) return toolNames[0] - if (toolNames.length === 2) return `${toolNames[0]}, ${toolNames[1]}` - return `${toolNames[0]}, ${toolNames[1]} +${toolNames.length - 2}` - }, [subBlock?.type, rawValue, customTools, workspaceId]) - - const filterDisplayValue = useMemo(() => { - const isFilterField = - subBlock?.id === 'filter' || subBlock?.id === 'filterCriteria' || subBlock?.id === 'sort' - - if (!isFilterField || !rawValue) return null - - const parsedValue = tryParseJson(rawValue) - - if (isPlainObject(parsedValue) || Array.isArray(parsedValue)) { - try { - const jsonStr = JSON.stringify(parsedValue, null, 0) - if (jsonStr.length <= 35) return jsonStr - return `${jsonStr.slice(0, 32)}...` - } catch { - return null - } - } - - return null - }, [subBlock?.id, rawValue]) + const filterDisplayValue = useMemo( + () => resolveFilterFieldLabel(subBlock, rawValue), + [subBlock, rawValue] + ) - /** - * Hydrates skill references to display names. - * Resolves skill IDs to their current names from the skills query. - */ + /** Hydrates skill references to display names. */ const { data: workspaceSkills = [] } = useSkills(workspaceId || '') - - const skillsDisplayValue = useMemo(() => { - if (subBlock?.type !== 'skill-input' || !Array.isArray(rawValue) || rawValue.length === 0) { - return null - } - - interface StoredSkill { - skillId: string - name?: string - } - - const skillNames = rawValue - .map((skill: StoredSkill) => { - if (!skill || typeof skill !== 'object') return null - - // Priority 1: Resolve skill name from the skills query (fresh data) - if (skill.skillId) { - const foundSkill = workspaceSkills.find((s) => s.id === skill.skillId) - if (foundSkill?.name) return foundSkill.name - } - - // Priority 2: Fall back to stored name (for deleted skills) - if (skill.name && typeof skill.name === 'string') return skill.name - - // Priority 3: Use skillId as last resort - if (skill.skillId) return skill.skillId - - return null - }) - .filter((name): name is string => !!name) - - if (skillNames.length === 0) return null - if (skillNames.length === 1) return skillNames[0] - if (skillNames.length === 2) return `${skillNames[0]}, ${skillNames[1]}` - return `${skillNames[0]}, ${skillNames[1]} +${skillNames.length - 2}` - }, [subBlock?.type, rawValue, workspaceSkills]) + const skillsDisplayValue = useMemo( + () => resolveSkillsLabel(subBlock, rawValue, workspaceSkills), + [subBlock, rawValue, workspaceSkills] + ) const isPasswordField = subBlock?.password === true const maskedValue = isPasswordField && value && value !== '-' ? '•••' : null diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/preview-workflow/components/block/block.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/preview-workflow/components/block/block.tsx index 3a488851474..7130ec1b7b6 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/preview-workflow/components/block/block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/preview-workflow/components/block/block.tsx @@ -3,14 +3,21 @@ import { type CSSProperties, memo, useMemo } from 'react' import { Handle, type NodeProps, Position } from 'reactflow' import { HANDLE_POSITIONS } from '@/lib/workflows/blocks/block-dimensions' +import { + getDisplayValue, + resolveDropdownLabel, + resolveSkillsLabel, + resolveToolsLabel, + resolveVariablesLabel, + resolveWorkflowMultiSelectLabel, + resolveWorkflowSelectionLabel, +} from '@/lib/workflows/subblocks/display' import { buildCanonicalIndex, evaluateSubBlockCondition, isSubBlockFeatureEnabled, isSubBlockVisibleForMode, } from '@/lib/workflows/subblocks/visibility' -import { DELETED_WORKFLOW_LABEL } from '@/app/workspace/[workspaceId]/logs/utils' -import { getDisplayValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block' import { getBlock } from '@/blocks' import { SELECTOR_TYPES_HYDRATION_REQUIRED, type SubBlockConfig } from '@/blocks/types' import { useVariablesStore } from '@/stores/variables/store' @@ -83,142 +90,6 @@ interface SubBlockRowProps { workflowLabelsReady: boolean } -/** - * Resolves dropdown/combobox value to its display label. - * Returns null if not a dropdown/combobox or no matching option found. - */ -function resolveDropdownLabel( - subBlock: SubBlockConfig | undefined, - rawValue: unknown -): string | null { - if (!subBlock || (subBlock.type !== 'dropdown' && subBlock.type !== 'combobox')) return null - if (!rawValue || typeof rawValue !== 'string') return null - - const options = typeof subBlock.options === 'function' ? subBlock.options() : subBlock.options - if (!options) return null - - const option = options.find((opt) => - typeof opt === 'string' ? opt === rawValue : opt.id === rawValue - ) - - if (!option) return null - return typeof option === 'string' ? option : option.label -} - -/** - * Resolves workflow ID to workflow name using the workflow registry. - * Uses synchronous store access to avoid hook dependencies. - */ -function resolveWorkflowName( - subBlock: SubBlockConfig | undefined, - rawValue: unknown, - workflowMap: Record, - workflowLabelsReady: boolean -): string | null { - if (subBlock?.type !== 'workflow-selector') return null - if (!rawValue || typeof rawValue !== 'string') return null - if (!workflowLabelsReady) return null - - return workflowMap[rawValue]?.name ?? DELETED_WORKFLOW_LABEL -} - -/** - * Type guard for variable assignments array - */ -function isVariableAssignmentsArray( - value: unknown -): value is Array<{ id?: string; variableId?: string; variableName?: string; value: unknown }> { - return ( - Array.isArray(value) && - value.length > 0 && - value.every( - (item) => - typeof item === 'object' && - item !== null && - ('variableName' in item || 'variableId' in item) - ) - ) -} - -/** - * Resolves variables-input to display names. - * Uses synchronous store access to avoid hook dependencies. - */ -function resolveVariablesDisplay( - subBlock: SubBlockConfig | undefined, - rawValue: unknown -): string | null { - if (subBlock?.type !== 'variables-input') return null - if (!isVariableAssignmentsArray(rawValue)) return null - - const variables = useVariablesStore.getState().variables - const variablesArray = Object.values(variables) - - const names = rawValue - .map((a) => { - if (a.variableId) { - const variable = variablesArray.find((v) => v.id === a.variableId) - return variable?.name - } - if (a.variableName) return a.variableName - return null - }) - .filter((name): name is string => !!name) - - if (names.length === 0) return null - if (names.length === 1) return names[0] - if (names.length === 2) return `${names[0]}, ${names[1]}` - return `${names[0]}, ${names[1]} +${names.length - 2}` -} - -/** - * Resolves tool-input to display names. - * Resolves built-in tools from block registry (no API needed). - */ -function resolveToolsDisplay( - subBlock: SubBlockConfig | undefined, - rawValue: unknown -): string | null { - if (subBlock?.type !== 'tool-input') return null - if (!Array.isArray(rawValue) || rawValue.length === 0) return null - - const toolNames = rawValue - .map((tool: unknown) => { - if (!tool || typeof tool !== 'object') return null - const t = tool as Record - - if (t.title && typeof t.title === 'string') return t.title - - const schema = t.schema as Record | undefined - if (schema?.function && typeof schema.function === 'object') { - const fn = schema.function as Record - if (fn.name && typeof fn.name === 'string') return fn.name - } - - const fn = t.function as Record | undefined - if (fn?.name && typeof fn.name === 'string') return fn.name - - if ( - typeof t.type === 'string' && - t.type !== 'custom-tool' && - t.type !== 'mcp' && - t.type !== 'workflow' && - t.type !== 'workflow_input' - ) { - const blockConfig = getBlock(t.type) - if (blockConfig?.name) return blockConfig.name - } - - return null - }) - .filter((name): name is string => !!name) - - if (toolNames.length === 0) return null - if (toolNames.length === 1) return toolNames[0] - if (toolNames.length === 2) return `${toolNames[0]}, ${toolNames[1]}` - return `${toolNames[0]}, ${toolNames[1]} +${toolNames.length - 2}` -} - /** * Renders a single subblock row with title and optional value. * Matches the SubBlockRow component in WorkflowBlock. @@ -226,7 +97,7 @@ function resolveToolsDisplay( * - Resolves dropdown/combobox labels * - Resolves workflow names from registry * - Resolves variable names from store - * - Resolves tool names from block registry + * - Resolves tool and skill names (registry + stored names; no API access) * - Shows '-' for other selector types that need hydration */ const SubBlockRow = memo(function SubBlockRow({ @@ -240,14 +111,37 @@ const SubBlockRow = memo(function SubBlockRow({ const isPasswordField = subBlock?.password === true const maskedValue = isPasswordField && value && value !== '-' ? '•••' : null + const workflowLookup = { workflowMap, ready: workflowLabelsReady } const dropdownLabel = resolveDropdownLabel(subBlock, rawValue) - const variablesDisplay = resolveVariablesDisplay(subBlock, rawValue) - const toolsDisplay = resolveToolsDisplay(subBlock, rawValue) - const workflowName = resolveWorkflowName(subBlock, rawValue, workflowMap, workflowLabelsReady) + // Materialize the variables store only for variables-input rows. + const variablesDisplay = + subBlock?.type === 'variables-input' + ? resolveVariablesLabel( + subBlock, + rawValue, + Object.values(useVariablesStore.getState().variables) + ) + : null + // The preview is hook-free, so custom tools referenced only by id resolve + // through their inline schema/registry fallbacks rather than the API. + const toolsDisplay = resolveToolsLabel(subBlock, rawValue, []) + const skillsDisplay = resolveSkillsLabel(subBlock, rawValue, []) + const workflowName = resolveWorkflowSelectionLabel(subBlock, rawValue, workflowLookup) + const workflowMultiSelectionNames = resolveWorkflowMultiSelectLabel( + subBlock, + rawValue, + workflowLookup + ) const isSelectorType = subBlock?.type && SELECTOR_TYPES_HYDRATION_REQUIRED.includes(subBlock.type) - const hydratedName = dropdownLabel || variablesDisplay || toolsDisplay || workflowName + const hydratedName = + dropdownLabel || + variablesDisplay || + toolsDisplay || + skillsDisplay || + workflowName || + workflowMultiSelectionNames const displayValue = maskedValue || hydratedName || (isSelectorType && value ? '-' : value) return ( diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/preview-workflow/preview-workflow.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/preview-workflow/preview-workflow.tsx index f8dddc56ab6..eb5a8fe2b81 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/preview-workflow/preview-workflow.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/preview-workflow/preview-workflow.tsx @@ -234,10 +234,12 @@ export function PreviewWorkflow({ const workspaceId = propWorkspaceId ?? params.workspaceId const { data: workflowMap = {}, - isLoading: isWorkflowMapLoading, + isSuccess: isWorkflowMapLoaded, isPlaceholderData: isWorkflowMapPlaceholderData, } = useWorkflowMap(workspaceId) - const workflowLabelsReady = !isWorkflowMapLoading && !isWorkflowMapPlaceholderData + // Ready only on a successful, non-placeholder load — an errored or stale + // placeholder map must not mislabel valid workflows as deleted. + const workflowLabelsReady = isWorkflowMapLoaded && !isWorkflowMapPlaceholderData const containerRef = useRef(null) const nodeTypes = previewNodeTypes const isValidWorkflowState = workflowState?.blocks && workflowState.edges diff --git a/apps/sim/background/workspace-notification-delivery.ts b/apps/sim/background/workspace-notification-delivery.ts deleted file mode 100644 index 76c2333d9fd..00000000000 --- a/apps/sim/background/workspace-notification-delivery.ts +++ /dev/null @@ -1,789 +0,0 @@ -import { db, workflowExecutionLogs } from '@sim/db' -import { - account, - workspaceNotificationDelivery, - workspaceNotificationSubscription, -} from '@sim/db/schema' -import { createLogger } from '@sim/logger' -import { hmacSha256Hex } from '@sim/security/hmac' -import { toError } from '@sim/utils/errors' -import { formatDuration } from '@sim/utils/formatting' -import { generateId } from '@sim/utils/id' -import { randomFloat } from '@sim/utils/random' -import { truncate } from '@sim/utils/string' -import { getActiveWorkflowContext } from '@sim/workflow-authz' -import { task } from '@trigger.dev/sdk' -import { and, eq, isNull, lte, or, sql } from 'drizzle-orm' -import { - type EmailRateLimitsData, - type EmailUsageData, - renderWorkflowNotificationEmail, -} from '@/components/emails' -import { checkUsageStatus } from '@/lib/billing/calculations/usage-monitor' -import { getHighestPrioritySubscription } from '@/lib/billing/core/subscription' -import { dollarsToCredits } from '@/lib/billing/credits/conversion' -import { RateLimiter } from '@/lib/core/rate-limiter' -import { decryptSecret } from '@/lib/core/security/encryption' -import { secureFetchWithValidation } from '@/lib/core/security/input-validation.server' -import { getBaseUrl } from '@/lib/core/utils/urls' -import { materializeExecutionData } from '@/lib/logs/execution/trace-store' -import type { TraceSpan, WorkflowExecutionLog } from '@/lib/logs/types' -import { sendEmail } from '@/lib/messaging/email/mailer' -import type { AlertConfig } from '@/lib/notifications/alert-rules' -import { getWorkspaceBilledAccountUserId } from '@/lib/workspaces/utils' - -const logger = createLogger('WorkspaceNotificationDelivery') - -const MAX_ATTEMPTS = 5 -const RETRY_DELAYS = [5 * 1000, 15 * 1000, 60 * 1000, 3 * 60 * 1000, 10 * 60 * 1000] -function getRetryDelayWithJitter(baseDelay: number): number { - const jitter = randomFloat() * 0.1 * baseDelay - return Math.floor(baseDelay + jitter) -} - -interface NotificationPayload { - id: string - type: 'workflow.execution.completed' - timestamp: number - data: { - workflowId: string - workflowName?: string - executionId: string - status: 'success' | 'error' - level: string - trigger: string - startedAt: string - endedAt: string - totalDurationMs: number - cost?: Record - finalOutput?: unknown - traceSpans?: TraceSpan[] - rateLimits?: EmailRateLimitsData - usage?: EmailUsageData - } -} - -function generateSignature(secret: string, timestamp: number, body: string): string { - const signatureBase = `${timestamp}.${body}` - return hmacSha256Hex(signatureBase, secret) -} - -async function buildPayload( - log: WorkflowExecutionLog, - subscription: typeof workspaceNotificationSubscription.$inferSelect -): Promise { - /** - * Skip notifications when the workflow or workspace has already been archived. - */ - if (!log.workflowId) return null - - const workflowContext = await getActiveWorkflowContext(log.workflowId) - - const timestamp = Date.now() - const executionData = (log.executionData || {}) as Record - if (!workflowContext?.workspaceId) { - return null - } - const userId = workflowContext.workspaceId - ? await getWorkspaceBilledAccountUserId(workflowContext.workspaceId) - : null - - const payload: NotificationPayload = { - id: `evt_${generateId()}`, - type: 'workflow.execution.completed', - timestamp, - data: { - workflowId: log.workflowId, - workflowName: workflowContext.workflow.name || 'Unknown Workflow', - executionId: log.executionId, - status: log.level === 'error' ? 'error' : 'success', - level: log.level, - trigger: log.trigger, - startedAt: log.startedAt, - endedAt: log.endedAt, - totalDurationMs: log.totalDurationMs, - cost: log.cost as Record, - }, - } - - if (subscription.includeFinalOutput && executionData.finalOutput) { - payload.data.finalOutput = executionData.finalOutput - } - - // Trace spans only included for webhooks (too large for email/Slack) - if ( - subscription.includeTraceSpans && - subscription.notificationType === 'webhook' && - executionData.traceSpans - ) { - payload.data.traceSpans = executionData.traceSpans as TraceSpan[] - } - - if (subscription.includeRateLimits && userId) { - try { - const userSubscription = await getHighestPrioritySubscription(userId) - const rateLimiter = new RateLimiter() - const triggerType = log.trigger === 'api' ? 'api' : 'manual' - - const [syncStatus, asyncStatus] = await Promise.all([ - rateLimiter.getRateLimitStatusWithSubscription( - userId, - userSubscription, - triggerType, - false - ), - rateLimiter.getRateLimitStatusWithSubscription(userId, userSubscription, triggerType, true), - ]) - - payload.data.rateLimits = { - sync: { - requestsPerMinute: syncStatus.requestsPerMinute, - maxBurst: syncStatus.maxBurst, - remaining: syncStatus.remaining, - resetAt: syncStatus.resetAt.toISOString(), - }, - async: { - requestsPerMinute: asyncStatus.requestsPerMinute, - maxBurst: asyncStatus.maxBurst, - remaining: asyncStatus.remaining, - resetAt: asyncStatus.resetAt.toISOString(), - }, - } - } catch (error) { - logger.warn('Failed to fetch rate limits for notification', { error, userId }) - } - } - - if (subscription.includeUsageData && userId) { - try { - const usageData = await checkUsageStatus(userId) - payload.data.usage = { - currentPeriodCost: usageData.currentUsage, - limit: usageData.limit, - percentUsed: usageData.percentUsed, - isExceeded: usageData.isExceeded, - } - } catch (error) { - logger.warn('Failed to fetch usage data for notification', { error, userId }) - } - } - - return payload -} - -interface WebhookConfig { - url: string - secret?: string -} - -interface SlackConfig { - channelId: string - channelName: string - accountId: string -} - -async function deliverWebhook( - subscription: typeof workspaceNotificationSubscription.$inferSelect, - payload: NotificationPayload -): Promise<{ success: boolean; status?: number; error?: string }> { - const webhookConfig = subscription.webhookConfig as WebhookConfig | null - if (!webhookConfig?.url) { - return { success: false, error: 'No webhook URL configured' } - } - - const body = JSON.stringify(payload) - const deliveryId = `delivery_${generateId()}` - const headers: Record = { - 'Content-Type': 'application/json', - 'sim-event': 'workflow.execution.completed', - 'sim-timestamp': payload.timestamp.toString(), - 'sim-delivery-id': deliveryId, - 'Idempotency-Key': deliveryId, - } - - if (webhookConfig.secret) { - const { decrypted } = await decryptSecret(webhookConfig.secret) - const signature = generateSignature(decrypted, payload.timestamp, body) - headers['sim-signature'] = `t=${payload.timestamp},v1=${signature}` - } - - try { - const response = await secureFetchWithValidation( - webhookConfig.url, - { - method: 'POST', - headers, - body, - timeout: 30000, - allowHttp: true, - }, - 'webhookUrl' - ) - - return { - success: response.ok, - status: response.status, - error: response.ok ? undefined : `HTTP ${response.status}`, - } - } catch (error: unknown) { - logger.warn('Webhook delivery failed', { - error: toError(error).message, - webhookUrl: webhookConfig.url, - }) - return { - success: false, - error: 'Failed to deliver webhook', - } - } -} - -function formatCost(cost?: Record): string { - if (!cost?.total) return 'N/A' - const total = cost.total as number - return `${dollarsToCredits(total).toLocaleString()} credits` -} - -function buildLogUrl(workspaceId: string, executionId: string): string { - return `${getBaseUrl()}/workspace/${workspaceId}/logs?executionId=${encodeURIComponent(executionId)}` -} - -function formatAlertReason(alertConfig: AlertConfig): string { - switch (alertConfig.rule) { - case 'consecutive_failures': - return `${alertConfig.consecutiveFailures} consecutive failures detected` - case 'failure_rate': - return `Failure rate exceeded ${alertConfig.failureRatePercent}% over ${alertConfig.windowHours}h` - case 'latency_threshold': - return `Execution exceeded ${Math.round((alertConfig.durationThresholdMs || 0) / 1000)}s duration threshold` - case 'latency_spike': - return `Execution was ${alertConfig.latencySpikePercent}% slower than average` - case 'cost_threshold': - return `Execution cost exceeded ${dollarsToCredits(alertConfig.costThresholdDollars || 0).toLocaleString()} credits threshold` - case 'no_activity': - return `No workflow activity detected in ${alertConfig.inactivityHours}h` - case 'error_count': - return `${alertConfig.errorCountThreshold} errors detected in ${alertConfig.windowHours}h window` - default: - return 'Alert condition met' - } -} - -async function deliverEmail( - subscription: typeof workspaceNotificationSubscription.$inferSelect, - payload: NotificationPayload, - alertConfig?: AlertConfig -): Promise<{ success: boolean; error?: string }> { - if (!subscription.emailRecipients || subscription.emailRecipients.length === 0) { - return { success: false, error: 'No email recipients configured' } - } - - const isError = payload.data.status !== 'success' - const statusText = isError ? 'Error' : 'Success' - const logUrl = buildLogUrl(subscription.workspaceId, payload.data.executionId) - const alertReason = alertConfig ? formatAlertReason(alertConfig) : undefined - - // Build subject line - const subject = alertReason - ? `Alert: ${payload.data.workflowName}` - : isError - ? `Error Alert: ${payload.data.workflowName}` - : `Workflow Completed: ${payload.data.workflowName}` - - // Build plain text for fallback - let includedDataText = '' - if (payload.data.finalOutput) { - includedDataText += `\n\nFinal Output:\n${JSON.stringify(payload.data.finalOutput, null, 2)}` - } - if (payload.data.rateLimits) { - includedDataText += `\n\nRate Limits:\n${JSON.stringify(payload.data.rateLimits, null, 2)}` - } - if (payload.data.usage) { - includedDataText += `\n\nUsage Data:\n${JSON.stringify(payload.data.usage, null, 2)}` - } - - // Render the email using the shared template - const html = await renderWorkflowNotificationEmail({ - workflowName: payload.data.workflowName || 'Unknown Workflow', - status: payload.data.status, - trigger: payload.data.trigger, - duration: formatDuration(payload.data.totalDurationMs, { precision: 1 }) ?? '-', - cost: formatCost(payload.data.cost), - logUrl, - alertReason, - finalOutput: payload.data.finalOutput, - rateLimits: payload.data.rateLimits, - usageData: payload.data.usage, - }) - - const result = await sendEmail({ - to: subscription.emailRecipients, - subject, - html, - text: `${subject}\n${alertReason ? `\nReason: ${alertReason}\n` : ''}\nWorkflow: ${payload.data.workflowName}\nStatus: ${statusText}\nTrigger: ${payload.data.trigger}\nDuration: ${formatDuration(payload.data.totalDurationMs, { precision: 1 }) ?? '-'}\nCost: ${formatCost(payload.data.cost)}\n\nView Log: ${logUrl}${includedDataText}`, - emailType: 'notifications', - }) - - return { success: result.success, error: result.success ? undefined : result.message } -} - -async function deliverSlack( - subscription: typeof workspaceNotificationSubscription.$inferSelect, - payload: NotificationPayload, - alertConfig?: AlertConfig -): Promise<{ success: boolean; error?: string }> { - const slackConfig = subscription.slackConfig as SlackConfig | null - if (!slackConfig?.channelId || !slackConfig?.accountId) { - return { success: false, error: 'No Slack channel or account configured' } - } - - const [slackAccount] = await db - .select({ accessToken: account.accessToken, userId: account.userId }) - .from(account) - .where(eq(account.id, slackConfig.accountId)) - .limit(1) - - if (!slackAccount?.accessToken) { - return { success: false, error: 'Slack account not found or not connected' } - } - - const alertReason = alertConfig ? formatAlertReason(alertConfig) : null - const statusEmoji = alertReason - ? ':warning:' - : payload.data.status === 'success' - ? ':white_check_mark:' - : ':x:' - const statusColor = alertReason - ? '#d97706' - : payload.data.status === 'success' - ? '#22c55e' - : '#ef4444' - const logUrl = buildLogUrl(subscription.workspaceId, payload.data.executionId) - - const blocks: Array> = [] - - if (alertReason) { - blocks.push({ - type: 'section', - text: { - type: 'mrkdwn', - text: `*Reason:* ${alertReason}`, - }, - }) - } - - blocks.push( - { - type: 'section', - fields: [ - { type: 'mrkdwn', text: `*Status:*\n${payload.data.status}` }, - { type: 'mrkdwn', text: `*Trigger:*\n${payload.data.trigger}` }, - { - type: 'mrkdwn', - text: `*Duration:*\n${formatDuration(payload.data.totalDurationMs, { precision: 1 }) ?? '-'}`, - }, - { type: 'mrkdwn', text: `*Cost:*\n${formatCost(payload.data.cost)}` }, - ], - }, - { - type: 'actions', - elements: [ - { - type: 'button', - text: { type: 'plain_text', text: 'View Log →', emoji: true }, - url: logUrl, - style: 'primary', - }, - ], - } - ) - - if (payload.data.finalOutput) { - const outputStr = JSON.stringify(payload.data.finalOutput, null, 2) - const truncated = truncate(outputStr, 2900) - blocks.push({ - type: 'section', - text: { - type: 'mrkdwn', - text: `*Final Output:*\n\`\`\`${truncated}\`\`\``, - }, - }) - } - - if (payload.data.rateLimits) { - const limitsStr = JSON.stringify(payload.data.rateLimits, null, 2) - blocks.push({ - type: 'section', - text: { - type: 'mrkdwn', - text: `*Rate Limits:*\n\`\`\`${limitsStr}\`\`\``, - }, - }) - } - - if (payload.data.usage) { - const usageStr = JSON.stringify(payload.data.usage, null, 2) - blocks.push({ - type: 'section', - text: { - type: 'mrkdwn', - text: `*Usage Data:*\n\`\`\`${usageStr}\`\`\``, - }, - }) - } - - blocks.push({ - type: 'context', - elements: [{ type: 'mrkdwn', text: `Execution ID: \`${payload.data.executionId}\`` }], - }) - - const fallbackText = alertReason - ? `⚠️ Alert: ${payload.data.workflowName} - ${alertReason}` - : `${payload.data.status === 'success' ? '✅' : '❌'} Workflow ${payload.data.workflowName}: ${payload.data.status}` - - const slackPayload = { - channel: slackConfig.channelId, - attachments: [{ color: statusColor, blocks }], - text: fallbackText, - } - - try { - const response = await fetch('https://slack.com/api/chat.postMessage', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${slackAccount.accessToken}`, - }, - body: JSON.stringify(slackPayload), - }) - - const result = await response.json() - - return { success: result.ok, error: result.ok ? undefined : result.error } - } catch (error: unknown) { - const err = error as Error - return { success: false, error: err.message } - } -} - -async function updateDeliveryStatus( - deliveryId: string, - status: 'success' | 'failed' | 'pending', - error?: string, - responseStatus?: number, - nextAttemptAt?: Date -) { - await db - .update(workspaceNotificationDelivery) - .set({ - status, - errorMessage: error || null, - responseStatus: responseStatus || null, - nextAttemptAt: nextAttemptAt || null, - updatedAt: new Date(), - }) - .where(eq(workspaceNotificationDelivery.id, deliveryId)) -} - -export interface NotificationDeliveryParams { - deliveryId: string - subscriptionId: string - workspaceId: string - notificationType: 'webhook' | 'email' | 'slack' - log: WorkflowExecutionLog - alertConfig?: AlertConfig -} - -export type NotificationDeliveryResult = - | { status: 'success' | 'skipped' | 'failed' } - | { status: 'retry'; retryDelayMs: number } - -function isRecord(value: unknown): value is Record { - return typeof value === 'object' && value !== null && !Array.isArray(value) -} - -function formatLogDate(value: Date | string | null | undefined, fallback = ''): string { - if (value instanceof Date) { - return value.toISOString() - } - return typeof value === 'string' ? value : fallback -} - -function normalizeLogFiles(value: unknown): WorkflowExecutionLog['files'] { - if (!Array.isArray(value)) { - return undefined - } - - return value.filter( - (file): file is NonNullable[number] => - isRecord(file) && - typeof file.id === 'string' && - typeof file.name === 'string' && - typeof file.size === 'number' && - typeof file.type === 'string' && - typeof file.url === 'string' && - typeof file.key === 'string' - ) -} - -async function normalizeWorkflowExecutionLog( - row: typeof workflowExecutionLogs.$inferSelect -): Promise { - const startedAt = formatLogDate(row.startedAt) - - // Heavy execution data may live in object storage; resolve the pointer so - // retry deliveries get finalOutput/traceSpans (no-op for inline rows). - const executionData = await materializeExecutionData( - isRecord(row.executionData) ? row.executionData : {}, - { workspaceId: row.workspaceId, workflowId: row.workflowId, executionId: row.executionId } - ) - - return { - id: row.id, - workflowId: row.workflowId, - executionId: row.executionId, - stateSnapshotId: row.stateSnapshotId, - level: row.level === 'error' ? 'error' : 'info', - trigger: row.trigger, - startedAt, - endedAt: formatLogDate(row.endedAt, startedAt), - totalDurationMs: row.totalDurationMs ?? 0, - files: normalizeLogFiles(row.files), - executionData: executionData as WorkflowExecutionLog['executionData'], - // cost_total projection of the usage_log ledger (not the deprecated jsonb). - cost: row.costTotal != null ? { total: Number(row.costTotal) } : undefined, - createdAt: formatLogDate(row.createdAt, startedAt), - } -} - -async function buildRetryLog(params: NotificationDeliveryParams): Promise { - const conditions = [eq(workflowExecutionLogs.executionId, params.log.executionId)] - if (params.log.workflowId) { - conditions.push(eq(workflowExecutionLogs.workflowId, params.log.workflowId)) - } - - const [storedLog] = await db - .select() - .from(workflowExecutionLogs) - .where(and(...conditions)) - .limit(1) - - if (storedLog) { - return await normalizeWorkflowExecutionLog(storedLog) - } - - const now = new Date().toISOString() - return { - id: `retry_log_${params.deliveryId}`, - workflowId: params.log.workflowId, - executionId: params.log.executionId, - stateSnapshotId: '', - level: 'info', - trigger: 'system', - startedAt: now, - endedAt: now, - totalDurationMs: 0, - executionData: {}, - cost: { total: 0 }, - createdAt: now, - } -} - -export async function enqueueNotificationDeliveryDispatch( - _params: NotificationDeliveryParams -): Promise { - return false -} - -const STUCK_IN_PROGRESS_THRESHOLD_MS = 5 * 60 * 1000 - -export async function sweepPendingNotificationDeliveries(limit = 50): Promise { - const stuckThreshold = new Date(Date.now() - STUCK_IN_PROGRESS_THRESHOLD_MS) - - await db - .update(workspaceNotificationDelivery) - .set({ - status: 'pending', - updatedAt: new Date(), - }) - .where( - and( - eq(workspaceNotificationDelivery.status, 'in_progress'), - lte(workspaceNotificationDelivery.lastAttemptAt, stuckThreshold) - ) - ) - - const dueDeliveries = await db - .select({ - deliveryId: workspaceNotificationDelivery.id, - subscriptionId: workspaceNotificationDelivery.subscriptionId, - workflowId: workspaceNotificationDelivery.workflowId, - executionId: workspaceNotificationDelivery.executionId, - workspaceId: workspaceNotificationSubscription.workspaceId, - alertConfig: workspaceNotificationSubscription.alertConfig, - notificationType: workspaceNotificationSubscription.notificationType, - }) - .from(workspaceNotificationDelivery) - .innerJoin( - workspaceNotificationSubscription, - eq(workspaceNotificationDelivery.subscriptionId, workspaceNotificationSubscription.id) - ) - .where( - and( - eq(workspaceNotificationDelivery.status, 'pending'), - or( - isNull(workspaceNotificationDelivery.nextAttemptAt), - lte(workspaceNotificationDelivery.nextAttemptAt, new Date()) - ) - ) - ) - .limit(limit) - - let enqueued = 0 - - for (const delivery of dueDeliveries) { - const params: NotificationDeliveryParams = { - deliveryId: delivery.deliveryId, - subscriptionId: delivery.subscriptionId, - workspaceId: delivery.workspaceId, - notificationType: delivery.notificationType, - log: await buildRetryLog({ - deliveryId: delivery.deliveryId, - subscriptionId: delivery.subscriptionId, - workspaceId: delivery.workspaceId, - notificationType: delivery.notificationType, - log: { - id: '', - workflowId: delivery.workflowId, - executionId: delivery.executionId, - stateSnapshotId: '', - level: 'info', - trigger: 'system', - startedAt: '', - endedAt: '', - totalDurationMs: 0, - executionData: {}, - cost: { total: 0 }, - createdAt: '', - }, - alertConfig: (delivery.alertConfig as AlertConfig | null) ?? undefined, - }), - alertConfig: (delivery.alertConfig as AlertConfig | null) ?? undefined, - } - - if (await enqueueNotificationDeliveryDispatch(params)) { - enqueued += 1 - } - } - - return enqueued -} - -export async function executeNotificationDelivery( - params: NotificationDeliveryParams -): Promise { - const { deliveryId, subscriptionId, notificationType, log, alertConfig } = params - - try { - const [subscription] = await db - .select() - .from(workspaceNotificationSubscription) - .where(eq(workspaceNotificationSubscription.id, subscriptionId)) - .limit(1) - - if (!subscription || !subscription.active) { - logger.warn(`Subscription ${subscriptionId} not found or inactive`) - await updateDeliveryStatus(deliveryId, 'failed', 'Subscription not found or inactive') - return { status: 'failed' } - } - - const claimed = await db - .update(workspaceNotificationDelivery) - .set({ - status: 'in_progress', - attempts: sql`${workspaceNotificationDelivery.attempts} + 1`, - lastAttemptAt: new Date(), - updatedAt: new Date(), - }) - .where( - and( - eq(workspaceNotificationDelivery.id, deliveryId), - eq(workspaceNotificationDelivery.status, 'pending'), - or( - isNull(workspaceNotificationDelivery.nextAttemptAt), - lte(workspaceNotificationDelivery.nextAttemptAt, new Date()) - ) - ) - ) - .returning({ attempts: workspaceNotificationDelivery.attempts }) - - if (claimed.length === 0) { - logger.info(`Delivery ${deliveryId} not claimable`) - return { status: 'skipped' } - } - - const attempts = claimed[0].attempts - const payload = await buildPayload(log, subscription) - - // Skip delivery for deleted workflows - if (!payload) { - await updateDeliveryStatus(deliveryId, 'failed', 'Workflow was archived or deleted') - logger.info(`Skipping delivery ${deliveryId} - workflow was archived or deleted`) - return { status: 'failed' } - } - - let result: { success: boolean; status?: number; error?: string } - - switch (notificationType) { - case 'webhook': - result = await deliverWebhook(subscription, payload) - break - case 'email': - result = await deliverEmail(subscription, payload, alertConfig) - break - case 'slack': - result = await deliverSlack(subscription, payload, alertConfig) - break - default: - result = { success: false, error: 'Unknown notification type' } - } - - if (result.success) { - await updateDeliveryStatus(deliveryId, 'success', undefined, result.status) - logger.info(`${notificationType} notification delivered successfully`, { deliveryId }) - return { status: 'success' } - } - if (attempts < MAX_ATTEMPTS) { - const retryDelay = getRetryDelayWithJitter( - RETRY_DELAYS[attempts - 1] || RETRY_DELAYS[RETRY_DELAYS.length - 1] - ) - const nextAttemptAt = new Date(Date.now() + retryDelay) - - await updateDeliveryStatus(deliveryId, 'pending', result.error, result.status, nextAttemptAt) - - logger.info( - `${notificationType} notification failed, scheduled retry ${attempts}/${MAX_ATTEMPTS}`, - { - deliveryId, - error: result.error, - } - ) - return { status: 'retry', retryDelayMs: retryDelay } - } - await updateDeliveryStatus(deliveryId, 'failed', result.error, result.status) - logger.error(`${notificationType} notification failed after ${MAX_ATTEMPTS} attempts`, { - deliveryId, - error: result.error, - }) - return { status: 'failed' } - } catch (error) { - logger.error('Notification delivery failed', { deliveryId, error }) - await updateDeliveryStatus(deliveryId, 'failed', 'Internal error') - return { status: 'failed' } - } -} - -export const workspaceNotificationDeliveryTask = task({ - id: 'workspace-notification-delivery', - retry: { maxAttempts: 1 }, - run: async (params: NotificationDeliveryParams) => executeNotificationDelivery(params), -}) diff --git a/apps/sim/blocks/blocks/enrichment.ts b/apps/sim/blocks/blocks/enrichment.ts index fea75a1937a..49b3d7f2a35 100644 --- a/apps/sim/blocks/blocks/enrichment.ts +++ b/apps/sim/blocks/blocks/enrichment.ts @@ -76,8 +76,8 @@ export const EnrichmentBlock: BlockConfig = { description: 'Enrich data with a Sim enrichment', longDescription: 'Run a Sim enrichment to look up data — work email, phone number, company domain, company info, and more — from the fields you map in. Uses the same provider cascade as table enrichments.', - docsLink: 'https://docs.sim.ai/tools/enrichment', - category: 'tools', + docsLink: 'https://docs.sim.ai/blocks/enrichment', + category: 'blocks', integrationType: IntegrationType.Sales, bgColor: '#9333EA', icon: EnrichmentIcon, diff --git a/apps/sim/blocks/blocks/logs.ts b/apps/sim/blocks/blocks/logs.ts index d7665089f98..e11aac9f73d 100644 --- a/apps/sim/blocks/blocks/logs.ts +++ b/apps/sim/blocks/blocks/logs.ts @@ -1,9 +1,11 @@ import { Library } from '@/components/emcn/icons' +import { fetchWorkspaceWorkflowOptions } from '@/lib/workflows/subblocks/options' import type { BlockConfig } from '@/blocks/types' export const LogsBlock: BlockConfig = { type: 'logs', name: 'Logs', + hideFromToolbar: true, description: 'Query workflow execution logs', longDescription: 'Search workflow execution logs in the current workspace, fetch a single log by id, or load full execution details with the per-block state snapshot.', @@ -251,3 +253,350 @@ export const LogsBlock: BlockConfig = { }, }, } + +const COMPARISON_OPERATOR_OPTIONS = [ + { label: '=', id: '=' }, + { label: '>', id: '>' }, + { label: '<', id: '<' }, + { label: '>=', id: '>=' }, + { label: '<=', id: '<=' }, + { label: '!=', id: '!=' }, +] + +/** Preset time windows mirroring the Logs page time-range filter. */ +const TIME_RANGE_MS: Record = { + 'past-30-minutes': 30 * 60 * 1000, + 'past-hour': 60 * 60 * 1000, + 'past-6-hours': 6 * 60 * 60 * 1000, + 'past-12-hours': 12 * 60 * 60 * 1000, + 'past-24-hours': 24 * 60 * 60 * 1000, + 'past-3-days': 3 * 24 * 60 * 60 * 1000, + 'past-7-days': 7 * 24 * 60 * 60 * 1000, + 'past-14-days': 14 * 24 * 60 * 60 * 1000, + 'past-30-days': 30 * 24 * 60 * 60 * 1000, +} + +/** Normalizes multi-select arrays or comma strings into a comma-separated string. */ +function joinIds(value: unknown): string | undefined { + if (Array.isArray(value)) { + const ids = value.filter((id): id is string => typeof id === 'string' && id.length > 0) + return ids.length > 0 ? ids.join(',') : undefined + } + if (typeof value === 'string' && value.trim().length > 0) return value.trim() + return undefined +} + +export const LogsV2Block: BlockConfig = { + type: 'logs_v2', + name: 'Logs', + description: 'Query workflow runs and fetch run details', + longDescription: + 'Query workflow run logs in the current workspace with the same filters as the Logs page, returning matching run IDs. Fetch full details for a single run, including its trace spans.', + bgColor: '#EAB308', + bestPractices: ` + - The block always operates on the current workspace; you cannot query other workspaces. + - 'Query Logs' returns only run IDs, ordered by the sort settings (newest first by default). Feed an ID into 'Get Run Details' for the full picture. + - 'Get Run Details' returns the run summary plus the full trace spans (per-block inputs, outputs, and timings). + - Cost filters and outputs are denominated in credits. + `, + icon: Library, + category: 'blocks', + docsLink: 'https://docs.sim.ai/blocks/logs', + subBlocks: [ + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + options: [ + { label: 'Query Logs', id: 'query' }, + { label: 'Get Run Details', id: 'get_run_details' }, + ], + value: () => 'query', + }, + { + id: 'workflowSelector', + title: 'Workflows', + type: 'dropdown', + multiSelect: true, + options: [], + placeholder: 'All workflows', + description: 'Only include runs of these workflows. Leave empty for all.', + mode: 'basic', + canonicalParamId: 'workflowIds', + condition: { field: 'operation', value: 'query' }, + fetchOptions: () => fetchWorkspaceWorkflowOptions(), + }, + { + id: 'manualWorkflowIds', + title: 'Workflow IDs', + type: 'short-input', + placeholder: 'Comma-separated workflow IDs', + mode: 'advanced', + canonicalParamId: 'workflowIds', + condition: { field: 'operation', value: 'query' }, + }, + { + id: 'level', + title: 'Status', + type: 'dropdown', + multiSelect: true, + options: [ + { label: 'Info', id: 'info' }, + { label: 'Error', id: 'error' }, + { label: 'Running', id: 'running' }, + { label: 'Pending', id: 'pending' }, + { label: 'Cancelled', id: 'cancelled' }, + ], + placeholder: 'All statuses', + condition: { field: 'operation', value: 'query' }, + }, + { + id: 'timeRange', + title: 'Time Range', + type: 'dropdown', + options: [ + { label: 'All time', id: 'all-time' }, + { label: 'Past 30 minutes', id: 'past-30-minutes' }, + { label: 'Past hour', id: 'past-hour' }, + { label: 'Past 6 hours', id: 'past-6-hours' }, + { label: 'Past 12 hours', id: 'past-12-hours' }, + { label: 'Past 24 hours', id: 'past-24-hours' }, + { label: 'Past 3 days', id: 'past-3-days' }, + { label: 'Past 7 days', id: 'past-7-days' }, + { label: 'Past 14 days', id: 'past-14-days' }, + { label: 'Past 30 days', id: 'past-30-days' }, + ], + value: () => 'all-time', + condition: { field: 'operation', value: 'query' }, + }, + { + id: 'startDate', + title: 'Start Date', + type: 'short-input', + placeholder: 'ISO 8601 timestamp (overrides Time Range)', + mode: 'advanced', + wandConfig: { + enabled: true, + prompt: + 'Generate an ISO 8601 timestamp from the user description. Return ONLY the timestamp string.', + generationType: 'timestamp', + }, + condition: { field: 'operation', value: 'query' }, + }, + { + id: 'endDate', + title: 'End Date', + type: 'short-input', + placeholder: 'ISO 8601 timestamp', + mode: 'advanced', + wandConfig: { + enabled: true, + prompt: + 'Generate an ISO 8601 timestamp from the user description. Return ONLY the timestamp string.', + generationType: 'timestamp', + }, + condition: { field: 'operation', value: 'query' }, + }, + { + id: 'costOperator', + title: 'Cost Comparison', + type: 'dropdown', + options: COMPARISON_OPERATOR_OPTIONS, + value: () => '>=', + mode: 'advanced', + condition: { field: 'operation', value: 'query' }, + }, + { + id: 'costValue', + title: 'Cost (credits)', + type: 'short-input', + placeholder: 'e.g. 10', + mode: 'advanced', + condition: { field: 'operation', value: 'query' }, + }, + { + id: 'durationOperator', + title: 'Duration Comparison', + type: 'dropdown', + options: COMPARISON_OPERATOR_OPTIONS, + value: () => '>=', + mode: 'advanced', + condition: { field: 'operation', value: 'query' }, + }, + { + id: 'durationValue', + title: 'Duration (ms)', + type: 'short-input', + placeholder: 'e.g. 30000', + mode: 'advanced', + condition: { field: 'operation', value: 'query' }, + }, + { + id: 'limit', + title: 'Limit', + type: 'short-input', + placeholder: '100 (max 200)', + condition: { field: 'operation', value: 'query' }, + }, + { + id: 'sortBy', + title: 'Sort By', + type: 'dropdown', + options: [ + { label: 'Date', id: 'date' }, + { label: 'Duration', id: 'duration' }, + { label: 'Cost', id: 'cost' }, + { label: 'Status', id: 'status' }, + ], + value: () => 'date', + mode: 'advanced', + condition: { field: 'operation', value: 'query' }, + }, + { + id: 'sortOrder', + title: 'Sort Order', + type: 'dropdown', + options: [ + { label: 'Descending', id: 'desc' }, + { label: 'Ascending', id: 'asc' }, + ], + value: () => 'desc', + mode: 'advanced', + condition: { field: 'operation', value: 'query' }, + }, + { + id: 'runId', + title: 'Run ID', + type: 'short-input', + placeholder: 'd864be57-0aa0-43b1-8fc3-e4ebb680572d', + condition: { field: 'operation', value: 'get_run_details' }, + required: true, + }, + ], + tools: { + access: ['logs_query_runs', 'logs_get_run_details'], + config: { + tool: (params: Record) => { + const operation = params.operation || 'query' + if (operation === 'get_run_details') return 'logs_get_run_details' + return 'logs_query_runs' + }, + params: (params: Record) => { + const operation = params.operation || 'query' + + if (operation === 'get_run_details') { + if (!params.runId) { + throw new Error('Logs Block Error: Run ID is required for Get Run Details') + } + return { runId: params.runId } + } + + const toNumber = (value: unknown): number | undefined => { + if (value === undefined || value === null || value === '') return undefined + const parsed = Number(value) + return Number.isFinite(parsed) ? parsed : undefined + } + + const timeRangeMs = TIME_RANGE_MS[params.timeRange] + const presetStartDate = timeRangeMs + ? new Date(Date.now() - timeRangeMs).toISOString() + : undefined + + const level = joinIds(params.level) + const costValue = toNumber(params.costValue) + const durationValue = toNumber(params.durationValue) + + return { + workflowIds: joinIds(params.workflowIds), + level, + startDate: params.startDate || presetStartDate, + endDate: params.endDate || undefined, + costOperator: costValue !== undefined ? params.costOperator || undefined : undefined, + costValue, + durationOperator: + durationValue !== undefined ? params.durationOperator || undefined : undefined, + durationValue, + limit: toNumber(params.limit), + sortBy: params.sortBy || undefined, + sortOrder: params.sortOrder || undefined, + } + }, + }, + }, + inputs: { + operation: { type: 'string', description: 'Operation to perform' }, + workflowIds: { type: 'array', description: 'Workflow IDs to filter by (canonical param)' }, + level: { type: 'array', description: 'Statuses to include (empty for all)' }, + timeRange: { type: 'string', description: 'Preset time window' }, + startDate: { type: 'string', description: 'ISO 8601 lower bound (overrides Time Range)' }, + endDate: { type: 'string', description: 'ISO 8601 upper bound' }, + costOperator: { type: 'string', description: "Cost comparison operator ('=', '>', …)" }, + costValue: { type: 'number', description: 'Cost threshold in credits' }, + durationOperator: { + type: 'string', + description: "Duration comparison operator ('=', '>', …)", + }, + durationValue: { type: 'number', description: 'Duration threshold in milliseconds' }, + limit: { type: 'number', description: 'Max run IDs to return (default 100, max 200)' }, + sortBy: { type: 'string', description: "'date' | 'duration' | 'cost' | 'status'" }, + sortOrder: { type: 'string', description: "'desc' | 'asc'" }, + runId: { type: 'string', description: 'Run ID (Get Run Details operation)' }, + }, + outputs: { + runIds: { + type: 'array', + description: 'IDs of the runs matching the filters', + condition: { field: 'operation', value: 'query' }, + }, + runId: { + type: 'string', + description: 'The run ID', + condition: { field: 'operation', value: 'get_run_details' }, + }, + workflowId: { + type: 'string', + description: 'Workflow ID this run belongs to', + condition: { field: 'operation', value: 'get_run_details' }, + }, + workflowName: { + type: 'string', + description: 'Workflow name', + condition: { field: 'operation', value: 'get_run_details' }, + }, + status: { + type: 'string', + description: 'Run status', + condition: { field: 'operation', value: 'get_run_details' }, + }, + trigger: { + type: 'string', + description: 'How the run was triggered', + condition: { field: 'operation', value: 'get_run_details' }, + }, + startedAt: { + type: 'string', + description: 'Run start time (ISO 8601)', + condition: { field: 'operation', value: 'get_run_details' }, + }, + durationMs: { + type: 'number', + description: 'Run duration in milliseconds', + condition: { field: 'operation', value: 'get_run_details' }, + }, + cost: { + type: 'number', + description: 'Run cost in credits', + condition: { field: 'operation', value: 'get_run_details' }, + }, + traceSpans: { + type: 'array', + description: 'Full trace spans for the run', + condition: { field: 'operation', value: 'get_run_details' }, + }, + finalOutput: { + type: 'json', + description: 'Final output of the run', + condition: { field: 'operation', value: 'get_run_details' }, + }, + }, +} diff --git a/apps/sim/blocks/blocks/mysql.ts b/apps/sim/blocks/blocks/mysql.ts index e60358c3ba8..33c0e32e129 100644 --- a/apps/sim/blocks/blocks/mysql.ts +++ b/apps/sim/blocks/blocks/mysql.ts @@ -11,7 +11,7 @@ export const MySQLBlock: BlockConfig = { longDescription: 'Integrate MySQL into the workflow. Can query, insert, update, delete, and execute raw SQL.', docsLink: 'https://docs.sim.ai/tools/mysql', - category: 'blocks', + category: 'tools', integrationType: IntegrationType.Databases, bgColor: '#FFFFFF', icon: MySQLIcon, diff --git a/apps/sim/blocks/blocks/postgresql.ts b/apps/sim/blocks/blocks/postgresql.ts index f15f72c076f..a453d73a26f 100644 --- a/apps/sim/blocks/blocks/postgresql.ts +++ b/apps/sim/blocks/blocks/postgresql.ts @@ -11,7 +11,7 @@ export const PostgreSQLBlock: BlockConfig = { longDescription: 'Integrate PostgreSQL into the workflow. Can query, insert, update, delete, and execute raw SQL.', docsLink: 'https://docs.sim.ai/tools/postgresql', - category: 'blocks', + category: 'tools', integrationType: IntegrationType.Databases, bgColor: '#336791', icon: PostgresIcon, diff --git a/apps/sim/blocks/blocks/sftp.ts b/apps/sim/blocks/blocks/sftp.ts index 5fca7e44d41..9b2ceb4171f 100644 --- a/apps/sim/blocks/blocks/sftp.ts +++ b/apps/sim/blocks/blocks/sftp.ts @@ -11,7 +11,7 @@ export const SftpBlock: BlockConfig = { longDescription: 'Upload, download, list, and manage files on remote servers via SFTP. Supports both password and private key authentication for secure file transfers.', docsLink: 'https://docs.sim.ai/tools/sftp', - category: 'blocks', + category: 'tools', integrationType: IntegrationType.Documents, bgColor: '#2D3748', icon: SftpIcon, diff --git a/apps/sim/blocks/blocks/sim_workspace_event.ts b/apps/sim/blocks/blocks/sim_workspace_event.ts new file mode 100644 index 00000000000..cec575d7049 --- /dev/null +++ b/apps/sim/blocks/blocks/sim_workspace_event.ts @@ -0,0 +1,40 @@ +import { SimTriggerIcon } from '@/components/icons' +import { SIM_WORKSPACE_EVENT_TRIGGER_ID } from '@/lib/workspace-events/constants' +import type { BlockConfig } from '@/blocks/types' +import { getTrigger } from '@/triggers' + +export const SimWorkspaceEventBlock: BlockConfig = { + // Literal (not SIM_WORKSPACE_EVENT_TRIGGER_ID) so scripts/generate-docs.ts + // can scrape the type for icon-map keys; a test asserts it stays equal to + // the constant. + type: 'sim_workspace_event', + name: 'Sim', + description: + 'Run this workflow when workspace events occur: execution errors or successes, deployments, and alert conditions like latency or cost spikes.', + category: 'triggers', + icon: SimTriggerIcon, + bgColor: '#33C482', + docsLink: 'https://docs.sim.ai/triggers/sim', + triggerAllowed: true, + bestPractices: ` + - Events are scoped to this workspace. Pick an event type, then optionally narrow to specific workflows (empty selection watches all). + - This workflow must be deployed for the trigger to fire, and it never receives events about itself. + - Executions started by this trigger never emit workspace events, so side-effect workflows cannot chain or loop. + - Alert conditions (latency spike, cost threshold, consecutive failures, ...) fire at most once per cooldown window; plain events fire on every occurrence. + - Compose any blocks downstream (Slack, email, webhooks, custom logic) to act on the event payload. + `, + subBlocks: [...getTrigger(SIM_WORKSPACE_EVENT_TRIGGER_ID).subBlocks], + + tools: { + access: [], + }, + + inputs: {}, + + outputs: {}, + + triggers: { + enabled: true, + available: [SIM_WORKSPACE_EVENT_TRIGGER_ID], + }, +} diff --git a/apps/sim/blocks/blocks/smtp.ts b/apps/sim/blocks/blocks/smtp.ts index 398053a6503..fb8aea2d887 100644 --- a/apps/sim/blocks/blocks/smtp.ts +++ b/apps/sim/blocks/blocks/smtp.ts @@ -11,7 +11,7 @@ export const SmtpBlock: BlockConfig = { longDescription: 'Send emails using any SMTP server (Gmail, Outlook, custom servers, etc.). Configure SMTP connection settings and send emails with full control over content, recipients, and attachments.', docsLink: 'https://docs.sim.ai/tools/smtp', - category: 'blocks', + category: 'tools', integrationType: IntegrationType.Email, bgColor: '#2D3748', icon: SmtpIcon, diff --git a/apps/sim/blocks/blocks/ssh.ts b/apps/sim/blocks/blocks/ssh.ts index f380d274067..0750b7ea24a 100644 --- a/apps/sim/blocks/blocks/ssh.ts +++ b/apps/sim/blocks/blocks/ssh.ts @@ -11,7 +11,7 @@ export const SSHBlock: BlockConfig = { longDescription: 'Execute commands, transfer files, and manage remote servers via SSH. Supports password and private key authentication for secure server access.', docsLink: 'https://docs.sim.ai/tools/ssh', - category: 'blocks', + category: 'tools', integrationType: IntegrationType.DevOps, bgColor: '#000000', icon: SshIcon, diff --git a/apps/sim/blocks/registry.ts b/apps/sim/blocks/registry.ts index 53e346953d9..c0a417e603e 100644 --- a/apps/sim/blocks/registry.ts +++ b/apps/sim/blocks/registry.ts @@ -160,7 +160,7 @@ import { LinearBlock, LinearBlockMeta, LinearV2Block } from '@/blocks/blocks/lin import { LinkedInBlock, LinkedInBlockMeta } from '@/blocks/blocks/linkedin' import { LinkupBlock, LinkupBlockMeta } from '@/blocks/blocks/linkup' import { LinqBlock, LinqBlockMeta } from '@/blocks/blocks/linq' -import { LogsBlock } from '@/blocks/blocks/logs' +import { LogsBlock, LogsV2Block } from '@/blocks/blocks/logs' import { LoopsBlock, LoopsBlockMeta } from '@/blocks/blocks/loops' import { LumaBlock, LumaBlockMeta } from '@/blocks/blocks/luma' import { MailchimpBlock, MailchimpBlockMeta } from '@/blocks/blocks/mailchimp' @@ -252,6 +252,7 @@ import { SESBlock, SESBlockMeta } from '@/blocks/blocks/ses' import { SftpBlock } from '@/blocks/blocks/sftp' import { SharepointBlock, SharepointBlockMeta, SharepointV2Block } from '@/blocks/blocks/sharepoint' import { ShopifyBlock, ShopifyBlockMeta } from '@/blocks/blocks/shopify' +import { SimWorkspaceEventBlock } from '@/blocks/blocks/sim_workspace_event' import { SimilarwebBlock, SimilarwebBlockMeta } from '@/blocks/blocks/similarweb' import { SixtyfourBlock, SixtyfourBlockMeta } from '@/blocks/blocks/sixtyfour' import { SlackBlock, SlackBlockMeta } from '@/blocks/blocks/slack' @@ -460,6 +461,7 @@ const BLOCK_REGISTRY: Record = { linkup: LinkupBlock, linq: LinqBlock, logs: LogsBlock, + logs_v2: LogsV2Block, loops: LoopsBlock, luma: LumaBlock, mailchimp: MailchimpBlock, @@ -539,6 +541,7 @@ const BLOCK_REGISTRY: Record = { sharepoint: SharepointBlock, sharepoint_v2: SharepointV2Block, shopify: ShopifyBlock, + sim_workspace_event: SimWorkspaceEventBlock, similarweb: SimilarwebBlock, sixtyfour: SixtyfourBlock, slack: SlackBlock, diff --git a/apps/sim/components/emails/index.ts b/apps/sim/components/emails/index.ts index d35bc5ebca1..ffbf4d11f22 100644 --- a/apps/sim/components/emails/index.ts +++ b/apps/sim/components/emails/index.ts @@ -8,8 +8,6 @@ export * from './billing' export * from './components' // Invitation emails export * from './invitations' -// Notification emails -export * from './notifications' // Render functions and subjects export * from './render' export * from './subjects' diff --git a/apps/sim/components/emails/notifications/index.ts b/apps/sim/components/emails/notifications/index.ts deleted file mode 100644 index 35da763fb59..00000000000 --- a/apps/sim/components/emails/notifications/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export type { - EmailRateLimitsData, - EmailUsageData, - WorkflowNotificationEmailProps, -} from './workflow-notification-email' -export { WorkflowNotificationEmail } from './workflow-notification-email' diff --git a/apps/sim/components/emails/notifications/workflow-notification-email.tsx b/apps/sim/components/emails/notifications/workflow-notification-email.tsx deleted file mode 100644 index 3389c8cad37..00000000000 --- a/apps/sim/components/emails/notifications/workflow-notification-email.tsx +++ /dev/null @@ -1,163 +0,0 @@ -import { Link, Section, Text } from '@react-email/components' -import { baseStyles } from '@/components/emails/_styles' -import { EmailLayout } from '@/components/emails/components' -import { dollarsToCredits } from '@/lib/billing/credits/conversion' -import { getBrandConfig } from '@/ee/whitelabeling' - -/** - * Serialized rate limit status for email payloads. - * Note: This differs from the canonical RateLimitStatus in @/lib/core/rate-limiter - * which uses Date for resetAt. This version uses string for JSON serialization. - */ -export interface EmailRateLimitStatus { - requestsPerMinute: number - remaining: number - maxBurst?: number - resetAt?: string -} - -export interface EmailRateLimitsData { - sync?: EmailRateLimitStatus - async?: EmailRateLimitStatus -} - -export interface EmailUsageData { - currentPeriodCost: number - limit: number - percentUsed: number - isExceeded?: boolean -} - -export interface WorkflowNotificationEmailProps { - workflowName: string - status: 'success' | 'error' - trigger: string - duration: string - cost: string - logUrl: string - alertReason?: string - finalOutput?: unknown - rateLimits?: EmailRateLimitsData - usageData?: EmailUsageData -} - -function formatJsonForEmail(data: unknown): string { - return JSON.stringify(data, null, 2) -} - -export function WorkflowNotificationEmail({ - workflowName, - status, - trigger, - duration, - cost, - logUrl, - alertReason, - finalOutput, - rateLimits, - usageData, -}: WorkflowNotificationEmailProps) { - const brand = getBrandConfig() - const isError = status === 'error' - const statusText = isError ? 'Error' : 'Success' - - const previewText = alertReason - ? `${brand.name}: Alert - ${workflowName}` - : isError - ? `${brand.name}: Workflow Failed - ${workflowName}` - : `${brand.name}: Workflow Completed - ${workflowName}` - - const message = alertReason - ? 'An alert was triggered for your workflow.' - : isError - ? 'Your workflow run failed.' - : 'Your workflow completed successfully.' - - return ( - - Hello, - {message} - -
- {alertReason && ( - - Reason: {alertReason} - - )} - - Workflow: {workflowName} - - - Status: {statusText} - - - Trigger: {trigger} - - - Duration: {duration} - - - Cost: {cost} - -
- - - View Run Log - - - {rateLimits && (rateLimits.sync || rateLimits.async) ? ( - <> -
-
- Rate Limits - {rateLimits.sync && ( - - Sync: {rateLimits.sync.remaining} of {rateLimits.sync.requestsPerMinute} remaining - - )} - {rateLimits.async && ( - - Async: {rateLimits.async.remaining} of {rateLimits.async.requestsPerMinute}{' '} - remaining - - )} -
- - ) : null} - - {usageData ? ( - <> -
-
- Usage - - {dollarsToCredits(usageData.currentPeriodCost).toLocaleString()} of{' '} - {dollarsToCredits(usageData.limit).toLocaleString()} credits used ( - {usageData.percentUsed.toFixed(1)}%) - -
- - ) : null} - - {finalOutput ? ( - <> -
-
- Final Output - - {formatJsonForEmail(finalOutput)} - -
- - ) : null} - -
- - - You're receiving this because you subscribed to workflow notifications. - - - ) -} - -export default WorkflowNotificationEmail diff --git a/apps/sim/components/emails/render.ts b/apps/sim/components/emails/render.ts index 645f5a056b0..2b3d416c51d 100644 --- a/apps/sim/components/emails/render.ts +++ b/apps/sim/components/emails/render.ts @@ -21,10 +21,6 @@ import { PollingGroupInvitationEmail, WorkspaceInvitationEmail, } from '@/components/emails/invitations' -import { - WorkflowNotificationEmail, - type WorkflowNotificationEmailProps, -} from '@/components/emails/notifications' import { HelpConfirmationEmail } from '@/components/emails/support' import { getBaseUrl } from '@/lib/core/utils/urls' @@ -249,9 +245,3 @@ export async function renderPaymentFailedEmail(params: { }) ) } - -export async function renderWorkflowNotificationEmail( - params: WorkflowNotificationEmailProps -): Promise { - return await render(WorkflowNotificationEmail(params)) -} diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index 41732af3b29..cdd2ab94016 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -6823,6 +6823,29 @@ export function SixtyfourIcon(props: SVGProps) { ) } +export function SimTriggerIcon(props: SVGProps) { + return ( + + ) +} + export function SimilarwebIcon(props: SVGProps) { return ( [...notificationKeys.all, 'list'] as const, - list: (workspaceId: string | undefined) => - [...notificationKeys.lists(), workspaceId ?? ''] as const, - details: () => [...notificationKeys.all, 'detail'] as const, - detail: (workspaceId: string, notificationId: string) => - [...notificationKeys.details(), workspaceId, notificationId] as const, -} - -/** - * Fetch notifications for a workspace - */ -async function fetchNotifications( - workspaceId: string, - signal?: AbortSignal -): Promise { - const data = await requestJson(listNotificationsContract, { - params: { id: workspaceId }, - signal, - }) - return data.data -} - -/** - * Hook to fetch notifications for a workspace - */ -export function useNotifications(workspaceId?: string) { - return useQuery({ - queryKey: notificationKeys.list(workspaceId), - queryFn: ({ signal }) => fetchNotifications(workspaceId!, signal), - enabled: Boolean(workspaceId), - staleTime: 30 * 1000, - placeholderData: keepPreviousData, - }) -} - -interface CreateNotificationParams { - workspaceId: string - data: ContractBodyInput -} - -/** - * Hook to create a notification - */ -export function useCreateNotification() { - const queryClient = useQueryClient() - - return useMutation({ - mutationFn: async ({ workspaceId, data }: CreateNotificationParams) => { - return requestJson(createNotificationContract, { - params: { id: workspaceId }, - body: data, - }) - }, - onSuccess: (_, { workspaceId }) => { - queryClient.invalidateQueries({ queryKey: notificationKeys.list(workspaceId) }) - }, - onError: (error) => { - logger.error('Failed to create notification', { error }) - }, - }) -} - -interface UpdateNotificationParams { - workspaceId: string - notificationId: string - data: ContractBodyInput -} - -/** - * Hook to update a notification - */ -export function useUpdateNotification() { - const queryClient = useQueryClient() - - return useMutation({ - mutationFn: async ({ workspaceId, notificationId, data }: UpdateNotificationParams) => { - return requestJson(updateNotificationContract, { - params: { id: workspaceId, notificationId }, - body: data, - }) - }, - onSuccess: (_, { workspaceId }) => { - queryClient.invalidateQueries({ queryKey: notificationKeys.list(workspaceId) }) - }, - onError: (error) => { - logger.error('Failed to update notification', { error }) - }, - }) -} - -interface DeleteNotificationParams { - workspaceId: string - notificationId: string -} - -/** - * Hook to delete a notification - */ -export function useDeleteNotification() { - const queryClient = useQueryClient() - - return useMutation({ - mutationFn: async ({ workspaceId, notificationId }: DeleteNotificationParams) => { - return requestJson(deleteNotificationContract, { - params: { id: workspaceId, notificationId }, - }) - }, - onSuccess: (_, { workspaceId }) => { - queryClient.invalidateQueries({ queryKey: notificationKeys.list(workspaceId) }) - }, - onError: (error) => { - logger.error('Failed to delete notification', { error }) - }, - }) -} - -interface TestNotificationParams { - workspaceId: string - notificationId: string -} - -/** - * Hook to test a notification - */ -export function useTestNotification() { - return useMutation({ - mutationFn: async ({ workspaceId, notificationId }: TestNotificationParams) => { - return requestJson(testNotificationContract, { - params: { id: workspaceId, notificationId }, - }) - }, - onError: (error) => { - logger.error('Failed to test notification', { error }) - }, - }) -} diff --git a/apps/sim/lib/api/contracts/index.ts b/apps/sim/lib/api/contracts/index.ts index 0001d2b51b9..85fd5fdb521 100644 --- a/apps/sim/lib/api/contracts/index.ts +++ b/apps/sim/lib/api/contracts/index.ts @@ -17,7 +17,6 @@ export * from './folders' export * from './hotspots' export * from './inbox' export * from './media' -export * from './notifications' export * from './permission-groups' export * from './primitives' export * from './selectors' diff --git a/apps/sim/lib/api/contracts/notifications.ts b/apps/sim/lib/api/contracts/notifications.ts deleted file mode 100644 index e3d2671cef8..00000000000 --- a/apps/sim/lib/api/contracts/notifications.ts +++ /dev/null @@ -1,300 +0,0 @@ -import { z } from 'zod' -import { defineRouteContract } from '@/lib/api/contracts/types' - -export const notificationWorkspaceParamsSchema = z.object({ - id: z.string().min(1), -}) - -export const notificationParamsSchema = z.object({ - id: z.string().min(1), - notificationId: z.string().min(1), -}) - -export const notificationTypeSchema = z.enum(['webhook', 'email', 'slack']) -export const notificationLevelSchema = z.enum(['info', 'error']) - -export const alertRuleSchema = z.enum([ - 'consecutive_failures', - 'failure_rate', - 'latency_threshold', - 'latency_spike', - 'cost_threshold', - 'no_activity', - 'error_count', -]) - -export const notificationAlertConfigSchema = z.object({ - rule: alertRuleSchema, - consecutiveFailures: z.number().int().optional(), - failureRatePercent: z.number().int().optional(), - windowHours: z.number().int().optional(), - durationThresholdMs: z.number().int().optional(), - latencySpikePercent: z.number().int().optional(), - costThresholdDollars: z.number().optional(), - inactivityHours: z.number().int().optional(), - errorCountThreshold: z.number().int().optional(), -}) - -export const notificationWebhookConfigSchema = z.object({ - url: z.string().url(), - secret: z.string().optional(), -}) - -export const notificationSlackConfigSchema = z.object({ - channelId: z.string(), - channelName: z.string(), - accountId: z.string(), -}) - -export type NotificationType = z.output -export type NotificationLogLevel = z.output -export type NotificationAlertRule = z.output -export type NotificationAlertConfig = z.output -export type NotificationWebhookConfig = z.output -export type NotificationSlackConfig = z.output - -export const notificationSubscriptionSchema = z.object({ - id: z.string(), - notificationType: notificationTypeSchema, - workflowIds: z.array(z.string()), - allWorkflows: z.boolean(), - levelFilter: z.array(notificationLevelSchema), - triggerFilter: z.array(z.string()), - includeFinalOutput: z.boolean(), - includeTraceSpans: z.boolean(), - includeRateLimits: z.boolean(), - includeUsageData: z.boolean(), - webhookConfig: notificationWebhookConfigSchema.nullish(), - emailRecipients: z.array(z.string()).nullish(), - slackConfig: notificationSlackConfigSchema.nullish(), - alertConfig: notificationAlertConfigSchema.nullish(), - active: z.boolean(), - createdAt: z.string(), - updatedAt: z.string(), -}) - -export type NotificationSubscription = z.output - -export const createNotificationBodySchema = z.object({ - notificationType: notificationTypeSchema, - workflowIds: z.array(z.string()), - allWorkflows: z.boolean(), - levelFilter: z.array(notificationLevelSchema), - triggerFilter: z.array(z.string()), - includeFinalOutput: z.boolean(), - includeTraceSpans: z.boolean(), - includeRateLimits: z.boolean(), - includeUsageData: z.boolean(), - alertConfig: notificationAlertConfigSchema.nullish(), - webhookConfig: notificationWebhookConfigSchema.optional(), - emailRecipients: z.array(z.string().email()).optional(), - slackConfig: notificationSlackConfigSchema.optional(), -}) - -export const updateNotificationBodySchema = createNotificationBodySchema - .omit({ notificationType: true }) - .partial() - .extend({ - active: z.boolean().optional(), - }) - -/** - * Server-side validation schemas with rule-specific refinements and bounded - * limits. These are stricter than the wire schemas above and are used by the - * `POST` and `PUT` notification routes to validate inbound payloads before - * persisting them. - */ -const serverAlertConfigSchema = z - .object({ - rule: alertRuleSchema, - consecutiveFailures: z.number().int().min(1).max(100).optional(), - failureRatePercent: z.number().int().min(1).max(100).optional(), - windowHours: z.number().int().min(1).max(168).optional(), - durationThresholdMs: z.number().int().min(1000).max(3600000).optional(), - latencySpikePercent: z.number().int().min(10).max(1000).optional(), - costThresholdDollars: z.number().min(0.01).max(1000).optional(), - inactivityHours: z.number().int().min(1).max(168).optional(), - errorCountThreshold: z.number().int().min(1).max(1000).optional(), - }) - .refine( - (data) => { - switch (data.rule) { - case 'consecutive_failures': - return data.consecutiveFailures !== undefined - case 'failure_rate': - return data.failureRatePercent !== undefined && data.windowHours !== undefined - case 'latency_threshold': - return data.durationThresholdMs !== undefined - case 'latency_spike': - return data.latencySpikePercent !== undefined && data.windowHours !== undefined - case 'cost_threshold': - return data.costThresholdDollars !== undefined - case 'no_activity': - return data.inactivityHours !== undefined - case 'error_count': - return data.errorCountThreshold !== undefined && data.windowHours !== undefined - default: - return false - } - }, - { message: 'Missing required fields for alert rule' } - ) - .nullable() - -export interface NotificationServerLimits { - maxEmailRecipients: number - maxWorkflowIds: number -} - -export const NOTIFICATION_SERVER_LIMITS: NotificationServerLimits = { - maxEmailRecipients: 10, - maxWorkflowIds: 1000, -} - -export function buildServerCreateNotificationSchema(limits: NotificationServerLimits) { - return z - .object({ - notificationType: notificationTypeSchema, - workflowIds: z.array(z.string()).max(limits.maxWorkflowIds).default([]), - allWorkflows: z.boolean().default(false), - levelFilter: z.array(notificationLevelSchema).default(['info', 'error']), - triggerFilter: z.array(z.string().min(1)).default([]), - includeFinalOutput: z.boolean().default(false), - includeTraceSpans: z.boolean().default(false), - includeRateLimits: z.boolean().default(false), - includeUsageData: z.boolean().default(false), - alertConfig: serverAlertConfigSchema.optional(), - webhookConfig: notificationWebhookConfigSchema.optional(), - emailRecipients: z.array(z.string().email()).max(limits.maxEmailRecipients).optional(), - slackConfig: notificationSlackConfigSchema.optional(), - }) - .refine( - (data) => { - if (data.notificationType === 'webhook') return !!data.webhookConfig?.url - if (data.notificationType === 'email') - return !!data.emailRecipients && data.emailRecipients.length > 0 - if (data.notificationType === 'slack') - return !!data.slackConfig?.channelId && !!data.slackConfig?.accountId - return false - }, - { message: 'Missing required fields for notification type' } - ) - .refine((data) => !(data.allWorkflows && data.workflowIds.length > 0), { - message: 'Cannot specify both allWorkflows and workflowIds', - }) -} - -export function buildServerUpdateNotificationSchema(limits: NotificationServerLimits) { - return z - .object({ - workflowIds: z.array(z.string()).max(limits.maxWorkflowIds).optional(), - allWorkflows: z.boolean().optional(), - levelFilter: z.array(notificationLevelSchema).optional(), - triggerFilter: z.array(z.string().min(1)).optional(), - includeFinalOutput: z.boolean().optional(), - includeTraceSpans: z.boolean().optional(), - includeRateLimits: z.boolean().optional(), - includeUsageData: z.boolean().optional(), - alertConfig: serverAlertConfigSchema.optional(), - webhookConfig: notificationWebhookConfigSchema.optional(), - emailRecipients: z.array(z.string().email()).max(limits.maxEmailRecipients).optional(), - slackConfig: notificationSlackConfigSchema.optional(), - active: z.boolean().optional(), - }) - .refine((data) => !(data.allWorkflows && data.workflowIds && data.workflowIds.length > 0), { - message: 'Cannot specify both allWorkflows and workflowIds', - }) -} - -export const listNotificationsContract = defineRouteContract({ - method: 'GET', - path: '/api/workspaces/[id]/notifications', - params: notificationWorkspaceParamsSchema, - response: { - mode: 'json', - schema: z.object({ - data: z.array(notificationSubscriptionSchema), - }), - }, -}) - -export const createNotificationContract = defineRouteContract({ - method: 'POST', - path: '/api/workspaces/[id]/notifications', - params: notificationWorkspaceParamsSchema, - body: createNotificationBodySchema, - response: { - mode: 'json', - schema: z.object({ - data: notificationSubscriptionSchema, - }), - }, -}) - -export const createNotificationServerContract = defineRouteContract({ - method: 'POST', - path: '/api/workspaces/[id]/notifications', - params: notificationWorkspaceParamsSchema, - body: buildServerCreateNotificationSchema(NOTIFICATION_SERVER_LIMITS), - response: { - mode: 'json', - schema: z.object({ - data: notificationSubscriptionSchema, - }), - }, -}) - -export const updateNotificationContract = defineRouteContract({ - method: 'PUT', - path: '/api/workspaces/[id]/notifications/[notificationId]', - params: notificationParamsSchema, - body: updateNotificationBodySchema, - response: { - mode: 'json', - schema: z.object({ - data: notificationSubscriptionSchema, - }), - }, -}) - -export const updateNotificationServerContract = defineRouteContract({ - method: 'PUT', - path: '/api/workspaces/[id]/notifications/[notificationId]', - params: notificationParamsSchema, - body: buildServerUpdateNotificationSchema(NOTIFICATION_SERVER_LIMITS), - response: { - mode: 'json', - schema: z.object({ - data: notificationSubscriptionSchema, - }), - }, -}) - -export const deleteNotificationContract = defineRouteContract({ - method: 'DELETE', - path: '/api/workspaces/[id]/notifications/[notificationId]', - params: notificationParamsSchema, - response: { - mode: 'json', - schema: z.object({ - success: z.literal(true), - }), - }, -}) - -export const testNotificationContract = defineRouteContract({ - method: 'POST', - path: '/api/workspaces/[id]/notifications/[notificationId]/test', - params: notificationParamsSchema, - response: { - mode: 'json', - schema: z.object({ - data: z.object({ - success: z.boolean(), - error: z.string().optional(), - channel: z.string().optional(), - timestamp: z.string().optional(), - }), - }), - }, -}) diff --git a/apps/sim/lib/integrations/icon-mapping.ts b/apps/sim/lib/integrations/icon-mapping.ts index 7704a069aca..6f4cf89271d 100644 --- a/apps/sim/lib/integrations/icon-mapping.ts +++ b/apps/sim/lib/integrations/icon-mapping.ts @@ -128,6 +128,7 @@ import { MistralIcon, MondayIcon, MongoDBIcon, + MySQLIcon, Neo4jIcon, NeverBounceIcon, NewRelicIcon, @@ -145,6 +146,7 @@ import { PineconeIcon, PipedriveIcon, PolymarketIcon, + PostgresIcon, PosthogIcon, ProfoundIcon, ProspeoIcon, @@ -172,11 +174,15 @@ import { SentryIcon, SerperIcon, ServiceNowIcon, + SftpIcon, ShopifyIcon, SimilarwebIcon, + SimTriggerIcon, SixtyfourIcon, SlackIcon, + SmtpIcon, SQSIcon, + SshIcon, STSIcon, STTIcon, StagehandIcon, @@ -335,6 +341,7 @@ export const blockTypeToIconMap: Record = { mistral_parse_v3: MistralIcon, monday: MondayIcon, mongodb: MongoDBIcon, + mysql: MySQLIcon, neo4j: Neo4jIcon, neverbounce: NeverBounceIcon, new_relic: NewRelicIcon, @@ -352,6 +359,7 @@ export const blockTypeToIconMap: Record = { pinecone: PineconeIcon, pipedrive: PipedriveIcon, polymarket: PolymarketIcon, + postgresql: PostgresIcon, posthog: PosthogIcon, profound: ProfoundIcon, prospeo: ProspeoIcon, @@ -379,12 +387,16 @@ export const blockTypeToIconMap: Record = { serper: SerperIcon, servicenow: ServiceNowIcon, ses: SESIcon, + sftp: SftpIcon, sharepoint_v2: MicrosoftSharepointIcon, shopify: ShopifyIcon, + sim_workspace_event: SimTriggerIcon, similarweb: SimilarwebIcon, sixtyfour: SixtyfourIcon, slack: SlackIcon, + smtp: SmtpIcon, sqs: SQSIcon, + ssh: SshIcon, stagehand: StagehandIcon, stripe: StripeIcon, sts: STSIcon, diff --git a/apps/sim/lib/integrations/integrations.json b/apps/sim/lib/integrations/integrations.json index dc45cec20f2..fac30504d2e 100644 --- a/apps/sim/lib/integrations/integrations.json +++ b/apps/sim/lib/integrations/integrations.json @@ -3338,24 +3338,6 @@ "integrationType": "observability", "tags": ["data-analytics", "automation"] }, - { - "type": "enrichment", - "slug": "data-enrichment", - "name": "Data Enrichment", - "description": "Enrich data with a Sim enrichment", - "longDescription": "Run a Sim enrichment to look up data — work email, phone number, company domain, company info, and more — from the fields you map in. Uses the same provider cascade as table enrichments.", - "bgColor": "#9333EA", - "iconName": "EnrichmentIcon", - "docsUrl": "https://docs.sim.ai/tools/enrichment", - "operations": [], - "operationCount": 0, - "triggers": [], - "triggerCount": 0, - "authType": "none", - "category": "tools", - "integrationType": "sales", - "tags": ["enrichment", "sales-engagement"] - }, { "type": "databricks", "slug": "databricks", @@ -9943,6 +9925,48 @@ "integrationType": "databases", "tags": ["data-warehouse", "cloud"] }, + { + "type": "mysql", + "slug": "mysql", + "name": "MySQL", + "description": "Connect to MySQL database", + "longDescription": "Integrate MySQL into the workflow. Can query, insert, update, delete, and execute raw SQL.", + "bgColor": "#FFFFFF", + "iconName": "MySQLIcon", + "docsUrl": "https://docs.sim.ai/tools/mysql", + "operations": [ + { + "name": "Query (SELECT)", + "description": "Execute SELECT query on MySQL database" + }, + { + "name": "Insert Data", + "description": "Insert new record into MySQL database" + }, + { + "name": "Update Data", + "description": "Update existing records in MySQL database" + }, + { + "name": "Delete Data", + "description": "Delete records from MySQL database" + }, + { + "name": "Execute Raw SQL", + "description": "Execute raw SQL query on MySQL database" + }, + { + "name": "Introspect Schema", + "description": "Introspect MySQL database schema to retrieve table structures, columns, and relationships" + } + ], + "operationCount": 6, + "triggers": [], + "triggerCount": 0, + "authType": "none", + "category": "tools", + "integrationType": "databases" + }, { "type": "neo4j", "slug": "neo4j", @@ -10783,6 +10807,48 @@ "integrationType": "analytics", "tags": ["prediction-markets", "data-analytics"] }, + { + "type": "postgresql", + "slug": "postgresql", + "name": "PostgreSQL", + "description": "Connect to PostgreSQL database", + "longDescription": "Integrate PostgreSQL into the workflow. Can query, insert, update, delete, and execute raw SQL.", + "bgColor": "#336791", + "iconName": "PostgresIcon", + "docsUrl": "https://docs.sim.ai/tools/postgresql", + "operations": [ + { + "name": "Query (SELECT)", + "description": "Execute a SELECT query on PostgreSQL database" + }, + { + "name": "Insert Data", + "description": "Insert data into PostgreSQL database" + }, + { + "name": "Update Data", + "description": "Update data in PostgreSQL database" + }, + { + "name": "Delete Data", + "description": "Delete data from PostgreSQL database" + }, + { + "name": "Execute Raw SQL", + "description": "Execute raw SQL query on PostgreSQL database" + }, + { + "name": "Introspect Schema", + "description": "Introspect PostgreSQL database schema to retrieve table structures, columns, and relationships" + } + ], + "operationCount": 6, + "triggers": [], + "triggerCount": 0, + "authType": "none", + "category": "tools", + "integrationType": "databases" + }, { "type": "posthog", "slug": "posthog", @@ -13385,6 +13451,48 @@ "integrationType": "support", "tags": ["customer-support", "ticketing", "incident-management"] }, + { + "type": "sftp", + "slug": "sftp", + "name": "SFTP", + "description": "Transfer files via SFTP (SSH File Transfer Protocol)", + "longDescription": "Upload, download, list, and manage files on remote servers via SFTP. Supports both password and private key authentication for secure file transfers.", + "bgColor": "#2D3748", + "iconName": "SftpIcon", + "docsUrl": "https://docs.sim.ai/tools/sftp", + "operations": [ + { + "name": "Upload Files", + "description": "Upload files to a remote SFTP server" + }, + { + "name": "Create File", + "description": "" + }, + { + "name": "Download File", + "description": "Download a file from a remote SFTP server" + }, + { + "name": "List Directory", + "description": "List files and directories on a remote SFTP server" + }, + { + "name": "Delete File/Directory", + "description": "Delete a file or directory on a remote SFTP server" + }, + { + "name": "Create Directory", + "description": "Create a directory on a remote SFTP server" + } + ], + "operationCount": 6, + "triggers": [], + "triggerCount": 0, + "authType": "api-key", + "category": "tools", + "integrationType": "documents" + }, { "type": "sharepoint_v2", "slug": "sharepoint", @@ -13807,6 +13915,93 @@ "aiDisclaimer": "Sim agents use AI models to generate messages and responses sent to Slack. AI-generated content can be inaccurate or incomplete — review automated outputs before relying on them, especially for important communications." } }, + { + "type": "smtp", + "slug": "smtp", + "name": "SMTP", + "description": "Send emails via any SMTP mail server", + "longDescription": "Send emails using any SMTP server (Gmail, Outlook, custom servers, etc.). Configure SMTP connection settings and send emails with full control over content, recipients, and attachments.", + "bgColor": "#2D3748", + "iconName": "SmtpIcon", + "docsUrl": "https://docs.sim.ai/tools/smtp", + "operations": [], + "operationCount": 0, + "triggers": [], + "triggerCount": 0, + "authType": "api-key", + "category": "tools", + "integrationType": "email" + }, + { + "type": "ssh", + "slug": "ssh", + "name": "SSH", + "description": "Connect to remote servers via SSH", + "longDescription": "Execute commands, transfer files, and manage remote servers via SSH. Supports password and private key authentication for secure server access.", + "bgColor": "#000000", + "iconName": "SshIcon", + "docsUrl": "https://docs.sim.ai/tools/ssh", + "operations": [ + { + "name": "Execute Command", + "description": "Execute a shell command on a remote SSH server" + }, + { + "name": "Execute Script", + "description": "Upload and execute a multi-line script on a remote SSH server" + }, + { + "name": "Check Command Exists", + "description": "Check if a command/program exists on the remote SSH server" + }, + { + "name": "Upload File", + "description": "Upload a file to a remote SSH server" + }, + { + "name": "Download File", + "description": "Download a file from a remote SSH server" + }, + { + "name": "List Directory", + "description": "List files and directories in a remote directory" + }, + { + "name": "Check File/Directory Exists", + "description": "Check if a file or directory exists on the remote SSH server" + }, + { + "name": "Create Directory", + "description": "Create a directory on the remote SSH server" + }, + { + "name": "Delete File/Directory", + "description": "Delete a file or directory from the remote SSH server" + }, + { + "name": "Move/Rename", + "description": "Move or rename a file or directory on the remote SSH server" + }, + { + "name": "Get System Info", + "description": "Retrieve system information from the remote SSH server" + }, + { + "name": "Read File Content", + "description": "Read the contents of a remote file" + }, + { + "name": "Write File Content", + "description": "Write or append content to a remote file" + } + ], + "operationCount": 13, + "triggers": [], + "triggerCount": 0, + "authType": "api-key", + "category": "tools", + "integrationType": "devops" + }, { "type": "stagehand", "slug": "stagehand", diff --git a/apps/sim/lib/logs/events.ts b/apps/sim/lib/logs/events.ts deleted file mode 100644 index 5a40fab190d..00000000000 --- a/apps/sim/lib/logs/events.ts +++ /dev/null @@ -1,165 +0,0 @@ -import { db } from '@sim/db' -import { workspaceNotificationDelivery, workspaceNotificationSubscription } from '@sim/db/schema' -import { createLogger } from '@sim/logger' -import { generateId } from '@sim/utils/id' -import { getActiveWorkflowContext } from '@sim/workflow-authz' -import { and, eq, or, sql } from 'drizzle-orm' -import { isTriggerDevEnabled } from '@/lib/core/config/feature-flags' -import type { WorkflowExecutionLog } from '@/lib/logs/types' -import { - type AlertCheckContext, - type AlertConfig, - shouldTriggerAlert, -} from '@/lib/notifications/alert-rules' -import { - executeNotificationDelivery, - workspaceNotificationDeliveryTask, -} from '@/background/workspace-notification-delivery' - -const logger = createLogger('LogsEventEmitter') - -function prepareLogData( - log: WorkflowExecutionLog, - subscription: { - includeFinalOutput: boolean - includeTraceSpans: boolean - } -) { - const preparedLog = { ...log, executionData: {} as Record } - - if (log.executionData) { - const data = log.executionData as Record - const webhookData: Record = {} - - if (subscription.includeFinalOutput && data.finalOutput) { - webhookData.finalOutput = data.finalOutput - } - - if (subscription.includeTraceSpans && data.traceSpans) { - webhookData.traceSpans = data.traceSpans - } - - preparedLog.executionData = webhookData - } - - return preparedLog -} - -export async function emitWorkflowExecutionCompleted(log: WorkflowExecutionLog): Promise { - try { - if (!log.workflowId) return - - const workflowContext = await getActiveWorkflowContext(log.workflowId) - if (!workflowContext?.workspaceId) return - - const workspaceId = workflowContext.workspaceId - - const subscriptions = await db - .select() - .from(workspaceNotificationSubscription) - .where( - and( - eq(workspaceNotificationSubscription.workspaceId, workspaceId), - eq(workspaceNotificationSubscription.active, true), - or( - eq(workspaceNotificationSubscription.allWorkflows, true), - sql`${log.workflowId} = ANY(${workspaceNotificationSubscription.workflowIds})` - ) - ) - ) - - if (subscriptions.length === 0) return - - logger.debug( - `Found ${subscriptions.length} active notification subscriptions for workspace ${workspaceId}` - ) - - for (const subscription of subscriptions) { - const levelMatches = subscription.levelFilter.includes(log.level) - const triggerMatches = - subscription.triggerFilter.length === 0 || subscription.triggerFilter.includes(log.trigger) - - if (!levelMatches || !triggerMatches) { - logger.debug(`Skipping subscription ${subscription.id} due to filter mismatch`) - continue - } - - const alertConfig = subscription.alertConfig as AlertConfig | null - - if (alertConfig) { - const context: AlertCheckContext = { - workflowId: log.workflowId, - executionId: log.executionId, - status: log.level === 'error' ? 'error' : 'success', - durationMs: log.totalDurationMs || 0, - cost: (log.cost as { total?: number })?.total || 0, - triggerFilter: subscription.triggerFilter, - } - - const shouldAlert = await shouldTriggerAlert(alertConfig, context, subscription.lastAlertAt) - - if (!shouldAlert) { - logger.debug(`Alert condition not met for subscription ${subscription.id}`) - continue - } - - await db - .update(workspaceNotificationSubscription) - .set({ lastAlertAt: new Date() }) - .where(eq(workspaceNotificationSubscription.id, subscription.id)) - - logger.info(`Alert triggered for subscription ${subscription.id}`, { - workflowId: log.workflowId, - alertConfig, - }) - } - - const deliveryId = generateId() - - await db.insert(workspaceNotificationDelivery).values({ - id: deliveryId, - subscriptionId: subscription.id, - workflowId: log.workflowId, - executionId: log.executionId, - status: 'pending', - attempts: 0, - nextAttemptAt: new Date(), - }) - - const notificationLog = prepareLogData(log, subscription) - - const payload = { - deliveryId, - subscriptionId: subscription.id, - workspaceId, - notificationType: subscription.notificationType, - log: notificationLog, - alertConfig: alertConfig || undefined, - } - - if (isTriggerDevEnabled) { - await workspaceNotificationDeliveryTask.trigger(payload, { - tags: [ - `workspaceId:${workspaceId}`, - `workflowId:${log.workflowId}`, - `notificationType:${subscription.notificationType}`, - ], - }) - logger.info( - `Enqueued ${subscription.notificationType} notification ${deliveryId} via Trigger.dev` - ) - } else { - void executeNotificationDelivery(payload).catch((error) => { - logger.error(`Direct notification delivery failed for ${deliveryId}`, { error }) - }) - logger.info(`Enqueued ${subscription.notificationType} notification ${deliveryId} directly`) - } - } - } catch (error) { - logger.error('Failed to emit workflow execution completed event', { - error, - workflowId: log.workflowId, - executionId: log.executionId, - }) - } -} diff --git a/apps/sim/lib/logs/execution/logger.test.ts b/apps/sim/lib/logs/execution/logger.test.ts index cf245552cc6..f016533b1ee 100644 --- a/apps/sim/lib/logs/execution/logger.test.ts +++ b/apps/sim/lib/logs/execution/logger.test.ts @@ -68,9 +68,9 @@ vi.mock('@/lib/core/utils/display-filters', () => ({ filterForDisplay: vi.fn((data) => data), })) -// Mock events -vi.mock('@/lib/logs/events', () => ({ - emitWorkflowExecutionCompleted: vi.fn(() => Promise.resolve()), +// Mock workspace event emission +vi.mock('@/lib/workspace-events/emitter', () => ({ + emitExecutionCompletedEvent: vi.fn(() => Promise.resolve()), })) // Mock snapshot service diff --git a/apps/sim/lib/logs/execution/logger.ts b/apps/sim/lib/logs/execution/logger.ts index 369b9397ce9..5c46172adcb 100644 --- a/apps/sim/lib/logs/execution/logger.ts +++ b/apps/sim/lib/logs/execution/logger.ts @@ -32,7 +32,6 @@ import { collectLargeValueReferenceKeys, replaceLargeValueReferenceKeysWithClient, } from '@/lib/execution/payloads/large-value-metadata' -import { emitWorkflowExecutionCompleted } from '@/lib/logs/events' import { snapshotService } from '@/lib/logs/execution/snapshot/service' import { externalizeExecutionData, @@ -50,6 +49,7 @@ import type { WorkflowExecutionSnapshot, WorkflowState, } from '@/lib/logs/types' +import { emitExecutionCompletedEvent } from '@/lib/workspace-events/emitter' import type { SerializableExecutionState } from '@/executor/execution/types' const logger = createLogger('ExecutionLogger') @@ -991,8 +991,8 @@ export class ExecutionLogger implements IExecutionLoggerService { createdAt: updatedLog.createdAt.toISOString(), } - emitWorkflowExecutionCompleted(completedLog).catch((error) => { - execLog.error('Failed to emit workflow execution completed event', { error }) + emitExecutionCompletedEvent(completedLog).catch((error) => { + execLog.error('Failed to emit workspace execution event', { error }) }) return completedLog diff --git a/apps/sim/lib/notifications/alert-rules.ts b/apps/sim/lib/notifications/alert-rules.ts deleted file mode 100644 index 708184cdac2..00000000000 --- a/apps/sim/lib/notifications/alert-rules.ts +++ /dev/null @@ -1,335 +0,0 @@ -import { db } from '@sim/db' -import { workflowExecutionLogs } from '@sim/db/schema' -import { createLogger } from '@sim/logger' -import { and, avg, count, desc, eq, gte, inArray } from 'drizzle-orm' - -const logger = createLogger('AlertRules') - -/** - * Alert rule types supported by the notification system - */ -export type AlertRuleType = - | 'consecutive_failures' - | 'failure_rate' - | 'latency_threshold' - | 'latency_spike' - | 'cost_threshold' - | 'no_activity' - | 'error_count' - -/** - * Configuration for alert rules - */ -export interface AlertConfig { - rule: AlertRuleType - consecutiveFailures?: number - failureRatePercent?: number - windowHours?: number - durationThresholdMs?: number - latencySpikePercent?: number - costThresholdDollars?: number - inactivityHours?: number - errorCountThreshold?: number -} - -/** - * Metadata for alert rule types - */ -interface AlertRuleDefinition { - type: AlertRuleType - name: string - description: string - requiredFields: (keyof AlertConfig)[] - defaultValues: Partial -} - -/** - * Registry of all alert rule definitions - */ -export const ALERT_RULES: Record = { - consecutive_failures: { - type: 'consecutive_failures', - name: 'Consecutive Failures', - description: 'Alert after X consecutive failed executions', - requiredFields: ['consecutiveFailures'], - defaultValues: { consecutiveFailures: 3 }, - }, - failure_rate: { - type: 'failure_rate', - name: 'Failure Rate', - description: 'Alert when failure rate exceeds X% over a time window', - requiredFields: ['failureRatePercent', 'windowHours'], - defaultValues: { failureRatePercent: 50, windowHours: 24 }, - }, - latency_threshold: { - type: 'latency_threshold', - name: 'Latency Threshold', - description: 'Alert when execution duration exceeds a threshold', - requiredFields: ['durationThresholdMs'], - defaultValues: { durationThresholdMs: 30000 }, - }, - latency_spike: { - type: 'latency_spike', - name: 'Latency Spike', - description: 'Alert when execution is X% slower than average', - requiredFields: ['latencySpikePercent', 'windowHours'], - defaultValues: { latencySpikePercent: 100, windowHours: 24 }, - }, - cost_threshold: { - type: 'cost_threshold', - name: 'Cost Threshold', - description: 'Alert when execution cost exceeds a threshold', - requiredFields: ['costThresholdDollars'], - defaultValues: { costThresholdDollars: 1 }, - }, - no_activity: { - type: 'no_activity', - name: 'No Activity', - description: 'Alert when no executions occur within a time window', - requiredFields: ['inactivityHours'], - defaultValues: { inactivityHours: 24 }, - }, - error_count: { - type: 'error_count', - name: 'Error Count', - description: 'Alert when error count exceeds threshold within time window', - requiredFields: ['errorCountThreshold', 'windowHours'], - defaultValues: { errorCountThreshold: 10, windowHours: 1 }, - }, -} - -/** - * Cooldown period in hours to prevent alert spam - */ -export const ALERT_COOLDOWN_HOURS = 1 - -/** - * Minimum executions required for rate-based alerts - */ -export const MIN_EXECUTIONS_FOR_RATE_ALERT = 5 - -/** - * Validates an alert configuration - */ -export function validateAlertConfig(config: AlertConfig): { valid: boolean; error?: string } { - const definition = ALERT_RULES[config.rule] - if (!definition) { - return { valid: false, error: `Unknown alert rule: ${config.rule}` } - } - - for (const field of definition.requiredFields) { - if (config[field] === undefined || config[field] === null) { - return { valid: false, error: `Missing required field: ${field}` } - } - } - - return { valid: true } -} - -/** - * Checks if a subscription is within its cooldown period - */ -export function isInCooldown(lastAlertAt: Date | null): boolean { - if (!lastAlertAt) return false - const cooldownEnd = new Date(lastAlertAt.getTime() + ALERT_COOLDOWN_HOURS * 60 * 60 * 1000) - return new Date() < cooldownEnd -} - -export interface AlertCheckContext { - workflowId: string - executionId: string - status: 'success' | 'error' - durationMs: number - cost: number - triggerFilter: string[] -} - -async function checkConsecutiveFailures( - workflowId: string, - threshold: number, - triggerFilter: string[] -): Promise { - const recentLogs = await db - .select({ level: workflowExecutionLogs.level }) - .from(workflowExecutionLogs) - .where( - and( - eq(workflowExecutionLogs.workflowId, workflowId), - inArray(workflowExecutionLogs.trigger, triggerFilter) - ) - ) - .orderBy(desc(workflowExecutionLogs.createdAt)) - .limit(threshold) - - if (recentLogs.length < threshold) return false - - return recentLogs.every((log) => log.level === 'error') -} - -async function checkFailureRate( - workflowId: string, - ratePercent: number, - windowHours: number, - triggerFilter: string[] -): Promise { - const windowStart = new Date(Date.now() - windowHours * 60 * 60 * 1000) - - const logs = await db - .select({ - level: workflowExecutionLogs.level, - createdAt: workflowExecutionLogs.createdAt, - }) - .from(workflowExecutionLogs) - .where( - and( - eq(workflowExecutionLogs.workflowId, workflowId), - gte(workflowExecutionLogs.createdAt, windowStart), - inArray(workflowExecutionLogs.trigger, triggerFilter) - ) - ) - .orderBy(workflowExecutionLogs.createdAt) - - if (logs.length < MIN_EXECUTIONS_FOR_RATE_ALERT) return false - - const oldestLog = logs[0] - if (oldestLog && oldestLog.createdAt > windowStart) { - return false - } - - const errorCount = logs.filter((log) => log.level === 'error').length - const failureRate = (errorCount / logs.length) * 100 - - return failureRate >= ratePercent -} - -/** - * Check if execution duration exceeds threshold - */ -function checkLatencyThreshold(durationMs: number, thresholdMs: number): boolean { - return durationMs > thresholdMs -} - -async function checkLatencySpike( - workflowId: string, - currentDurationMs: number, - spikePercent: number, - windowHours: number, - triggerFilter: string[] -): Promise { - const windowStart = new Date(Date.now() - windowHours * 60 * 60 * 1000) - - const result = await db - .select({ - avgDuration: avg(workflowExecutionLogs.totalDurationMs), - count: count(), - }) - .from(workflowExecutionLogs) - .where( - and( - eq(workflowExecutionLogs.workflowId, workflowId), - gte(workflowExecutionLogs.createdAt, windowStart), - inArray(workflowExecutionLogs.trigger, triggerFilter) - ) - ) - - const avgDuration = result[0]?.avgDuration - const execCount = result[0]?.count || 0 - - if (!avgDuration || execCount < MIN_EXECUTIONS_FOR_RATE_ALERT) return false - - const avgMs = Number(avgDuration) - const threshold = avgMs * (1 + spikePercent / 100) - - return currentDurationMs > threshold -} - -/** - * Check if execution cost exceeds threshold - */ -function checkCostThreshold(cost: number, thresholdDollars: number): boolean { - return cost > thresholdDollars -} - -async function checkErrorCount( - workflowId: string, - threshold: number, - windowHours: number, - triggerFilter: string[] -): Promise { - const windowStart = new Date(Date.now() - windowHours * 60 * 60 * 1000) - - const result = await db - .select({ count: count() }) - .from(workflowExecutionLogs) - .where( - and( - eq(workflowExecutionLogs.workflowId, workflowId), - eq(workflowExecutionLogs.level, 'error'), - gte(workflowExecutionLogs.createdAt, windowStart), - inArray(workflowExecutionLogs.trigger, triggerFilter) - ) - ) - - const errorCount = result[0]?.count || 0 - return errorCount >= threshold -} - -export async function shouldTriggerAlert( - config: AlertConfig, - context: AlertCheckContext, - lastAlertAt: Date | null -): Promise { - if (isInCooldown(lastAlertAt)) { - logger.debug('Subscription in cooldown, skipping alert check') - return false - } - - const { rule } = config - const { workflowId, status, durationMs, cost, triggerFilter } = context - - switch (rule) { - case 'consecutive_failures': - if (status !== 'error') return false - return checkConsecutiveFailures(workflowId, config.consecutiveFailures!, triggerFilter) - - case 'failure_rate': - if (status !== 'error') return false - return checkFailureRate( - workflowId, - config.failureRatePercent!, - config.windowHours!, - triggerFilter - ) - - case 'latency_threshold': - return checkLatencyThreshold(durationMs, config.durationThresholdMs!) - - case 'latency_spike': - return checkLatencySpike( - workflowId, - durationMs, - config.latencySpikePercent!, - config.windowHours!, - triggerFilter - ) - - case 'cost_threshold': - return checkCostThreshold(cost, config.costThresholdDollars!) - - case 'no_activity': - return false - - case 'error_count': - if (status !== 'error') return false - return checkErrorCount( - workflowId, - config.errorCountThreshold!, - config.windowHours!, - triggerFilter - ) - - default: - logger.warn(`Unknown alert rule: ${rule}`) - return false - } -} diff --git a/apps/sim/lib/notifications/inactivity-polling.ts b/apps/sim/lib/notifications/inactivity-polling.ts deleted file mode 100644 index 254d07be462..00000000000 --- a/apps/sim/lib/notifications/inactivity-polling.ts +++ /dev/null @@ -1,294 +0,0 @@ -import { db } from '@sim/db' -import { - workflow, - workflowDeploymentVersion, - workflowExecutionLogs, - workspaceNotificationDelivery, - workspaceNotificationSubscription, -} from '@sim/db/schema' -import { createLogger } from '@sim/logger' -import { generateId } from '@sim/utils/id' -import { and, eq, gte, inArray, sql } from 'drizzle-orm' -import { isTriggerDevEnabled } from '@/lib/core/config/feature-flags' -import { TRIGGER_TYPES } from '@/lib/workflows/triggers/triggers' -import { - executeNotificationDelivery, - workspaceNotificationDeliveryTask, -} from '@/background/workspace-notification-delivery' -import type { WorkflowState } from '@/stores/workflows/workflow/types' -import type { AlertConfig } from './alert-rules' -import { isInCooldown } from './alert-rules' - -const logger = createLogger('InactivityPolling') - -const SCHEDULE_BLOCK_TYPES: string[] = [TRIGGER_TYPES.SCHEDULE] -const WEBHOOK_BLOCK_TYPES: string[] = [TRIGGER_TYPES.WEBHOOK, TRIGGER_TYPES.GENERIC_WEBHOOK] - -function deploymentHasTriggerType( - deploymentState: Pick, - triggerFilter: string[] -): boolean { - const blocks = deploymentState.blocks - if (!blocks) return false - - const alwaysAvailable = ['api', 'manual', 'chat'] - if (triggerFilter.some((t) => alwaysAvailable.includes(t))) { - return true - } - - for (const block of Object.values(blocks)) { - if (triggerFilter.includes('schedule') && SCHEDULE_BLOCK_TYPES.includes(block.type)) { - return true - } - - if (triggerFilter.includes('webhook')) { - if (WEBHOOK_BLOCK_TYPES.includes(block.type)) { - return true - } - if (block.triggerMode === true) { - return true - } - } - } - - return false -} - -async function getWorkflowsWithTriggerTypes( - workspaceId: string, - triggerFilter: string[] -): Promise> { - const workflowIds = new Set() - - const deployedWorkflows = await db - .select({ - workflowId: workflow.id, - deploymentState: workflowDeploymentVersion.state, - }) - .from(workflow) - .innerJoin( - workflowDeploymentVersion, - and( - eq(workflowDeploymentVersion.workflowId, workflow.id), - eq(workflowDeploymentVersion.isActive, true) - ) - ) - .where(and(eq(workflow.workspaceId, workspaceId), eq(workflow.isDeployed, true))) - - for (const w of deployedWorkflows) { - const state = w.deploymentState as WorkflowState | null - if (state && deploymentHasTriggerType(state, triggerFilter)) { - workflowIds.add(w.workflowId) - } - } - - return workflowIds -} - -interface InactivityCheckResult { - subscriptionId: string - workflowId: string - triggered: boolean - reason?: string -} - -async function checkWorkflowInactivity( - subscription: typeof workspaceNotificationSubscription.$inferSelect, - workflowId: string, - alertConfig: AlertConfig -): Promise { - const result: InactivityCheckResult = { - subscriptionId: subscription.id, - workflowId, - triggered: false, - } - - if (isInCooldown(subscription.lastAlertAt)) { - result.reason = 'in_cooldown' - return result - } - - const windowStart = new Date(Date.now() - (alertConfig.inactivityHours || 24) * 60 * 60 * 1000) - const triggerFilter = subscription.triggerFilter - const levelFilter = subscription.levelFilter - - const recentLogs = await db - .select({ id: workflowExecutionLogs.id }) - .from(workflowExecutionLogs) - .where( - and( - eq(workflowExecutionLogs.workflowId, workflowId), - gte(workflowExecutionLogs.createdAt, windowStart), - inArray(workflowExecutionLogs.trigger, triggerFilter), - inArray(workflowExecutionLogs.level, levelFilter) - ) - ) - .limit(1) - - if (recentLogs.length > 0) { - result.reason = 'has_activity' - return result - } - - const [workflowData] = await db - .select({ - name: workflow.name, - workspaceId: workflow.workspaceId, - }) - .from(workflow) - .where(eq(workflow.id, workflowId)) - .limit(1) - - if (!workflowData || !workflowData.workspaceId) { - result.reason = 'workflow_not_found' - return result - } - - await db - .update(workspaceNotificationSubscription) - .set({ lastAlertAt: new Date() }) - .where(eq(workspaceNotificationSubscription.id, subscription.id)) - - const deliveryId = generateId() - - await db.insert(workspaceNotificationDelivery).values({ - id: deliveryId, - subscriptionId: subscription.id, - workflowId, - executionId: `inactivity_${Date.now()}`, - status: 'pending', - attempts: 0, - nextAttemptAt: new Date(), - }) - - const now = new Date().toISOString() - const mockLog = { - id: `inactivity_log_${generateId()}`, - workflowId, - executionId: `inactivity_${Date.now()}`, - stateSnapshotId: '', - level: 'info' as const, - trigger: 'system' as const, - startedAt: now, - endedAt: now, - totalDurationMs: 0, - executionData: {}, - cost: { total: 0 }, - workspaceId: workflowData.workspaceId, - createdAt: now, - } - - const payload = { - deliveryId, - subscriptionId: subscription.id, - workspaceId: workflowData.workspaceId, - notificationType: subscription.notificationType, - log: mockLog, - alertConfig, - } - - if (isTriggerDevEnabled) { - await workspaceNotificationDeliveryTask.trigger(payload, { - tags: [ - `workspaceId:${workflowData.workspaceId}`, - `workflowId:${workflowId}`, - `notificationType:${subscription.notificationType}`, - ], - }) - } else { - void executeNotificationDelivery(payload).catch((error) => { - logger.error(`Direct notification delivery failed for ${deliveryId}`, { error }) - }) - } - - result.triggered = true - result.reason = 'alert_sent' - - logger.info(`Inactivity alert triggered for workflow ${workflowId}`, { - subscriptionId: subscription.id, - inactivityHours: alertConfig.inactivityHours, - }) - - return result -} - -export async function pollInactivityAlerts(): Promise<{ - total: number - triggered: number - skipped: number - details: InactivityCheckResult[] -}> { - logger.info('Starting inactivity alert polling') - - const subscriptions = await db - .select() - .from(workspaceNotificationSubscription) - .where( - and( - eq(workspaceNotificationSubscription.active, true), - sql`${workspaceNotificationSubscription.alertConfig}->>'rule' = 'no_activity'` - ) - ) - - if (subscriptions.length === 0) { - logger.info('No active no_activity subscriptions found') - return { total: 0, triggered: 0, skipped: 0, details: [] } - } - - logger.info(`Found ${subscriptions.length} no_activity subscriptions to check`) - - const results: InactivityCheckResult[] = [] - let triggered = 0 - let skipped = 0 - - for (const subscription of subscriptions) { - const alertConfig = subscription.alertConfig as AlertConfig - if (!alertConfig || alertConfig.rule !== 'no_activity') { - continue - } - - const triggerFilter = subscription.triggerFilter as string[] - if (!triggerFilter || triggerFilter.length === 0) { - logger.warn(`Subscription ${subscription.id} has no trigger filter, skipping`) - continue - } - - const eligibleWorkflowIds = await getWorkflowsWithTriggerTypes( - subscription.workspaceId, - triggerFilter - ) - - let workflowIds: string[] = [] - - if (subscription.allWorkflows) { - workflowIds = Array.from(eligibleWorkflowIds) - } else { - workflowIds = (subscription.workflowIds || []).filter((id) => eligibleWorkflowIds.has(id)) - } - - logger.debug(`Checking ${workflowIds.length} workflows for subscription ${subscription.id}`, { - triggerFilter, - eligibleCount: eligibleWorkflowIds.size, - }) - - for (const workflowId of workflowIds) { - const result = await checkWorkflowInactivity(subscription, workflowId, alertConfig) - results.push(result) - - if (result.triggered) { - triggered++ - } else { - skipped++ - } - } - } - - logger.info(`Inactivity polling completed: ${triggered} alerts triggered, ${skipped} skipped`) - - return { - total: results.length, - triggered, - skipped, - details: results, - } -} diff --git a/apps/sim/lib/webhooks/processor.ts b/apps/sim/lib/webhooks/processor.ts index 96ce09813b4..3352048b94f 100644 --- a/apps/sim/lib/webhooks/processor.ts +++ b/apps/sim/lib/webhooks/processor.ts @@ -19,6 +19,7 @@ import { } from '@/lib/webhooks/pending-verification' import { getProviderHandler } from '@/lib/webhooks/providers' import { blockExistsInDeployment } from '@/lib/workflows/persistence/utils' +import { SIM_TRIGGER_PROVIDER } from '@/lib/workspace-events/constants' import { executeWebhookJob } from '@/background/webhook-execution' import { resolveEnvVarReferences } from '@/executor/utils/reference-validation' import { isPollingWebhookProvider } from '@/triggers/constants' @@ -772,7 +773,9 @@ export async function processPolledWebhookEvent( ...(credentialId ? { credentialId } : {}), } - if (isPollingWebhookProvider(payload.provider) && !shouldExecuteInline()) { + const isQueueRoutedProvider = + isPollingWebhookProvider(payload.provider) || payload.provider === SIM_TRIGGER_PROVIDER + if (isQueueRoutedProvider && !shouldExecuteInline()) { const jobId = await (await getJobQueue()).enqueue('webhook-execution', payload, { metadata: { workflowId: foundWorkflow.id, diff --git a/apps/sim/lib/workflows/blocks/block-outputs.ts b/apps/sim/lib/workflows/blocks/block-outputs.ts index 15fb4161b70..15e130bdd66 100644 --- a/apps/sim/lib/workflows/blocks/block-outputs.ts +++ b/apps/sim/lib/workflows/blocks/block-outputs.ts @@ -267,8 +267,15 @@ export function getBlockOutputs( if (triggerId && isTriggerValid(triggerId)) { const trigger = getTrigger(triggerId) if (trigger.outputs) { - // TriggerOutput is compatible with OutputFieldDefinition at runtime - return trigger.outputs as OutputDefinition + // TriggerOutput is compatible with OutputFieldDefinition at runtime. + // Conditions narrow outputs to the selected trigger configuration + // (e.g. the Sim trigger only surfaces execution fields for + // execution-backed event types). + return filterOutputsByCondition( + trigger.outputs as OutputDefinition, + subBlocks, + includeHidden + ) } } } diff --git a/apps/sim/lib/workflows/orchestration/deploy.test.ts b/apps/sim/lib/workflows/orchestration/deploy.test.ts index 2fff78e9122..ffe40b6fc9d 100644 --- a/apps/sim/lib/workflows/orchestration/deploy.test.ts +++ b/apps/sim/lib/workflows/orchestration/deploy.test.ts @@ -10,6 +10,11 @@ const { mockRecordAudit, mockCaptureServerEvent, mockTransaction, + mockDeployWorkflow, + mockActivateWorkflowVersion, + mockValidateWorkflowSchedules, + mockValidateTriggerWebhookConfigForDeploy, + mockEmitWorkflowDeployedEvent, mockTx, } = vi.hoisted(() => ({ mockLimit: vi.fn(), @@ -18,6 +23,11 @@ const { mockRecordAudit: vi.fn(), mockCaptureServerEvent: vi.fn(), mockTransaction: vi.fn(), + mockDeployWorkflow: vi.fn(), + mockActivateWorkflowVersion: vi.fn(), + mockValidateWorkflowSchedules: vi.fn(), + mockValidateTriggerWebhookConfigForDeploy: vi.fn(), + mockEmitWorkflowDeployedEvent: vi.fn(), mockTx: { select: vi.fn(() => ({ from: vi.fn(() => ({ @@ -59,11 +69,26 @@ vi.mock('@sim/db', () => ({ })) vi.mock('@sim/audit', () => ({ - AuditAction: { WORKFLOW_DEPLOYMENT_REVERTED: 'WORKFLOW_DEPLOYMENT_REVERTED' }, + AuditAction: { + WORKFLOW_DEPLOYMENT_REVERTED: 'WORKFLOW_DEPLOYMENT_REVERTED', + WORKFLOW_DEPLOYED: 'WORKFLOW_DEPLOYED', + WORKFLOW_UNDEPLOYED: 'WORKFLOW_UNDEPLOYED', + WORKFLOW_DEPLOYMENT_ACTIVATED: 'WORKFLOW_DEPLOYMENT_ACTIVATED', + }, AuditResourceType: { WORKFLOW: 'WORKFLOW' }, recordAudit: mockRecordAudit, })) +vi.mock('@/lib/workflows/deployment-outbox', () => ({ + enqueueWorkflowDeploymentSideEffects: vi.fn().mockResolvedValue('outbox-1'), + enqueueWorkflowUndeploySideEffects: vi.fn().mockResolvedValue('outbox-2'), + processWorkflowDeploymentOutboxEvent: vi.fn().mockResolvedValue('completed'), +})) + +vi.mock('@/lib/workspace-events/emitter', () => ({ + emitWorkflowDeployedEvent: mockEmitWorkflowDeployedEvent, +})) + vi.mock('@/lib/core/config/env', () => ({ env: { INTERNAL_API_SECRET: 'secret' }, })) @@ -78,9 +103,9 @@ vi.mock('@/lib/posthog/server', () => ({ })) vi.mock('@/lib/workflows/persistence/utils', () => ({ - activateWorkflowVersion: vi.fn(), + activateWorkflowVersion: mockActivateWorkflowVersion, activateWorkflowVersionById: vi.fn(), - deployWorkflow: vi.fn(), + deployWorkflow: mockDeployWorkflow, loadWorkflowDeploymentSnapshot: vi.fn(), saveWorkflowToNormalizedTables: mockSaveWorkflowToNormalizedTables, undeployWorkflow: vi.fn(), @@ -95,15 +120,20 @@ vi.mock('@/lib/webhooks/deploy', () => ({ cleanupWebhooksForWorkflow: vi.fn(), restorePreviousVersionWebhooks: vi.fn(), saveTriggerWebhooksForDeploy: vi.fn(), + validateTriggerWebhookConfigForDeploy: mockValidateTriggerWebhookConfigForDeploy, })) vi.mock('@/lib/workflows/schedules', () => ({ cleanupDeploymentVersion: vi.fn(), createSchedulesForDeploy: vi.fn(), - validateWorkflowSchedules: vi.fn(), + validateWorkflowSchedules: mockValidateWorkflowSchedules, })) -import { performRevertToVersion } from '@/lib/workflows/orchestration/deploy' +import { + performActivateVersion, + performFullDeploy, + performRevertToVersion, +} from '@/lib/workflows/orchestration/deploy' describe('performRevertToVersion', () => { beforeEach(() => { @@ -209,3 +239,122 @@ describe('performRevertToVersion', () => { expect(Object.hasOwn(workflowUpdate, 'variables')).toBe(false) }) }) + +describe('performFullDeploy workspace event emission', () => { + beforeEach(() => { + vi.clearAllMocks() + vi.stubGlobal('fetch', vi.fn().mockResolvedValue(new Response(null, { status: 200 }))) + mockLimit.mockResolvedValue([ + { id: 'workflow-1', name: 'My Workflow', workspaceId: 'workspace-1' }, + ]) + mockDeployWorkflow.mockResolvedValue({ + success: true, + deployedAt: new Date(), + version: 4, + deploymentVersionId: 'dv-1', + previousVersionId: null, + currentState: { blocks: {} }, + }) + }) + + it('emits workflow_deployed after a successful deploy', async () => { + const result = await performFullDeploy({ + workflowId: 'workflow-1', + userId: 'user-1', + }) + + expect(result.success).toBe(true) + expect(mockEmitWorkflowDeployedEvent).toHaveBeenCalledTimes(1) + expect(mockEmitWorkflowDeployedEvent).toHaveBeenCalledWith({ + workflowId: 'workflow-1', + workflowName: 'My Workflow', + workspaceId: 'workspace-1', + version: 4, + }) + }) + + it('does not emit when the deploy fails', async () => { + mockDeployWorkflow.mockResolvedValueOnce({ success: false, error: 'nope' }) + + const result = await performFullDeploy({ + workflowId: 'workflow-1', + userId: 'user-1', + }) + + expect(result.success).toBe(false) + expect(mockEmitWorkflowDeployedEvent).not.toHaveBeenCalled() + }) + + it('emission rejection does not fail the deploy', async () => { + mockEmitWorkflowDeployedEvent.mockRejectedValueOnce(new Error('emit failed')) + + const result = await performFullDeploy({ + workflowId: 'workflow-1', + userId: 'user-1', + }) + + expect(result.success).toBe(true) + }) +}) + +describe('performActivateVersion workspace event emission', () => { + beforeEach(() => { + vi.clearAllMocks() + vi.stubGlobal('fetch', vi.fn().mockResolvedValue(new Response(null, { status: 200 }))) + mockValidateWorkflowSchedules.mockReturnValue({ isValid: true }) + mockValidateTriggerWebhookConfigForDeploy.mockResolvedValue({ success: true }) + mockLimit.mockResolvedValue([{ id: 'dv-2', state: { blocks: {} }, isActive: false }]) + mockActivateWorkflowVersion.mockResolvedValue({ + success: true, + deployedAt: new Date(), + previousVersionId: 'dv-1', + }) + }) + + it('emits workflow_deployed when activating a version (rollback/activation)', async () => { + const result = await performActivateVersion({ + workflowId: 'workflow-1', + version: 2, + userId: 'user-1', + workflow: { id: 'workflow-1', name: 'My Workflow', workspaceId: 'workspace-1' }, + }) + + expect(result.success).toBe(true) + expect(mockEmitWorkflowDeployedEvent).toHaveBeenCalledWith({ + workflowId: 'workflow-1', + workflowName: 'My Workflow', + workspaceId: 'workspace-1', + version: 2, + }) + }) + + it('does not emit when the version is already active (no-op activation)', async () => { + mockLimit + .mockResolvedValueOnce([{ id: 'dv-2', state: { blocks: {} }, isActive: true }]) + .mockResolvedValueOnce([{ deployedAt: new Date() }]) + + const result = await performActivateVersion({ + workflowId: 'workflow-1', + version: 2, + userId: 'user-1', + workflow: { id: 'workflow-1', name: 'My Workflow', workspaceId: 'workspace-1' }, + }) + + expect(result.success).toBe(true) + expect(mockEmitWorkflowDeployedEvent).not.toHaveBeenCalled() + }) + + it('does not emit when activation fails', async () => { + mockActivateWorkflowVersion.mockResolvedValueOnce({ success: false, error: 'nope' }) + + const result = await performActivateVersion({ + workflowId: 'workflow-1', + version: 2, + userId: 'user-1', + workflow: { id: 'workflow-1', name: 'My Workflow', workspaceId: 'workspace-1' }, + }) + + expect(result.success).toBe(false) + expect(mockEmitWorkflowDeployedEvent).not.toHaveBeenCalled() + }) +}) diff --git a/apps/sim/lib/workflows/orchestration/deploy.ts b/apps/sim/lib/workflows/orchestration/deploy.ts index 1b6089cc5b3..16ba0be08a3 100644 --- a/apps/sim/lib/workflows/orchestration/deploy.ts +++ b/apps/sim/lib/workflows/orchestration/deploy.ts @@ -22,6 +22,7 @@ import { undeployWorkflow, } from '@/lib/workflows/persistence/utils' import { validateWorkflowSchedules } from '@/lib/workflows/schedules' +import { emitWorkflowDeployedEvent } from '@/lib/workspace-events/emitter' import type { BlockState, WorkflowState } from '@/stores/workflows/workflow/types' const logger = createLogger('DeployOrchestration') @@ -188,6 +189,16 @@ export async function performFullDeploy( const sideEffectWarning = await processDeploymentSideEffectsNow(outboxEventId, requestId) await notifySocketDeploymentChanged(workflowId) + const workspaceId = workflowData.workspaceId as string | null + if (workspaceId) { + void emitWorkflowDeployedEvent({ + workflowId, + workflowName: (workflowData.name as string) || workflowId, + workspaceId, + version: deployResult.version ?? null, + }) + } + return { success: true, deployedAt, @@ -419,6 +430,16 @@ export async function performActivateVersion( const sideEffectWarning = await processDeploymentSideEffectsNow(outboxEventId, requestId) await notifySocketDeploymentChanged(workflowId) + const activationWorkspaceId = (workflow.workspaceId as string) || null + if (activationWorkspaceId) { + void emitWorkflowDeployedEvent({ + workflowId, + workflowName: (workflow.name as string) || workflowId, + workspaceId: activationWorkspaceId, + version, + }) + } + return { success: true, deployedAt: result.deployedAt, diff --git a/apps/sim/lib/workflows/subblocks/display.test.ts b/apps/sim/lib/workflows/subblocks/display.test.ts new file mode 100644 index 00000000000..305c3cd6dd6 --- /dev/null +++ b/apps/sim/lib/workflows/subblocks/display.test.ts @@ -0,0 +1,183 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it, vi } from 'vitest' + +vi.mock('@/blocks', () => ({ + getBlock: (type: string) => (type === 'slack' ? { name: 'Slack' } : undefined), +})) + +import { + getDisplayValue, + resolveDropdownLabel, + resolveFilterFieldLabel, + resolveSkillsLabel, + resolveToolsLabel, + resolveVariablesLabel, + resolveWorkflowMultiSelectLabel, + resolveWorkflowSelectionLabel, + summarizeNames, +} from '@/lib/workflows/subblocks/display' +import type { SubBlockConfig } from '@/blocks/types' + +const workflowSelector = { id: 'workflowId', type: 'workflow-selector' } as SubBlockConfig +const workflowMulti = { + id: 'workflowIds', + type: 'dropdown', + multiSelect: true, +} as SubBlockConfig +const variablesInput = { id: 'variables', type: 'variables-input' } as SubBlockConfig +const toolInput = { id: 'tools', type: 'tool-input' } as SubBlockConfig +const skillInput = { id: 'skills', type: 'skill-input' } as SubBlockConfig + +describe('summarizeNames', () => { + it('formats 0, 1, 2, and 2+N name lists', () => { + expect(summarizeNames([])).toBeNull() + expect(summarizeNames(['A'])).toBe('A') + expect(summarizeNames(['A', 'B'])).toBe('A, B') + expect(summarizeNames(['A', 'B', 'C', 'D'])).toBe('A, B +2') + }) +}) + +describe('workflow selection labels', () => { + const lookup = { workflowMap: { 'wf-1': { name: 'Billing' } }, ready: true } + + it('resolves a single workflow selection to its name', () => { + expect(resolveWorkflowSelectionLabel(workflowSelector, 'wf-1', lookup)).toBe('Billing') + }) + + it('labels missing workflows as deleted only after the lookup is ready', () => { + expect(resolveWorkflowSelectionLabel(workflowSelector, 'wf-gone', lookup)).toBe( + 'Deleted Workflow' + ) + expect( + resolveWorkflowSelectionLabel(workflowSelector, 'wf-gone', { ...lookup, ready: false }) + ).toBeNull() + }) + + it('summarizes multi-select workflow ids with the deleted fallback', () => { + expect(resolveWorkflowMultiSelectLabel(workflowMulti, ['wf-1', 'wf-gone'], lookup)).toBe( + 'Billing, Deleted Workflow' + ) + expect( + resolveWorkflowMultiSelectLabel(workflowMulti, ['wf-1'], { ...lookup, ready: false }) + ).toBeNull() + }) + + it('matches multi-select subblocks by canonicalParamId as well as id', () => { + const canonical = { + id: 'workflowSelector', + type: 'dropdown', + multiSelect: true, + canonicalParamId: 'workflowIds', + } as SubBlockConfig + expect(resolveWorkflowMultiSelectLabel(canonical, ['wf-1'], lookup)).toBe('Billing') + }) +}) + +describe('resolveVariablesLabel', () => { + it('resolves variable ids to live names and falls back to stored names', () => { + const variables = [{ id: 'var-1', name: 'apiKey' }] + expect( + resolveVariablesLabel( + variablesInput, + [ + { variableId: 'var-1', value: 1 }, + { variableName: 'region', value: 2 }, + ], + variables + ) + ).toBe('apiKey, region') + }) +}) + +describe('resolveToolsLabel', () => { + it('resolves titles, custom tools by id, schema names, and registry blocks', () => { + const customTools = [{ id: 'ct-1', title: 'My Tool' }] + expect( + resolveToolsLabel( + toolInput, + [ + { title: 'Explicit' }, + { type: 'custom-tool', customToolId: 'ct-1' }, + { schema: { function: { name: 'inline_fn' } } }, + { type: 'slack' }, + ], + customTools + ) + ).toBe('Explicit, My Tool +2') + }) + + it('skips unresolvable entries instead of inventing labels', () => { + expect(resolveToolsLabel(toolInput, [{ type: 'custom-tool', customToolId: 'gone' }], [])).toBe( + null + ) + }) +}) + +describe('resolveSkillsLabel', () => { + it('prefers live skill names and falls back to the stored name', () => { + const skills = [{ id: 'sk-1', name: 'Research' }] + expect( + resolveSkillsLabel( + skillInput, + [{ skillId: 'sk-1' }, { skillId: 'sk-deleted', name: 'Old Name' }], + skills + ) + ).toBe('Research, Old Name') + }) + + it('never renders raw skill ids', () => { + expect(resolveSkillsLabel(skillInput, [{ skillId: 'sk-unknown' }], [])).toBeNull() + }) +}) + +describe('resolveDropdownLabel', () => { + const dropdown = { + id: 'mode', + type: 'dropdown', + options: [{ id: 'opt-1', label: 'Option One' }, 'literal'], + } as SubBlockConfig + + it('resolves option ids and string options to labels', () => { + expect(resolveDropdownLabel(dropdown, 'opt-1')).toBe('Option One') + expect(resolveDropdownLabel(dropdown, 'literal')).toBe('literal') + expect(resolveDropdownLabel(dropdown, 'missing')).toBeNull() + }) +}) + +describe('resolveFilterFieldLabel', () => { + const filterField = { id: 'filter', type: 'short-input' } as SubBlockConfig + + it('renders compact JSON for filter fields and truncates long values', () => { + expect(resolveFilterFieldLabel(filterField, '{"a":1}')).toBe('{"a":1}') + const long = JSON.stringify({ column: 'status', operator: 'contains', value: 'running' }) + expect(resolveFilterFieldLabel(filterField, long)).toBe(`${long.slice(0, 32)}...`) + }) + + it('returns null for non-filter subblocks and non-JSON values', () => { + expect( + resolveFilterFieldLabel({ id: 'other', type: 'short-input' } as SubBlockConfig, '{"a":1}') + ).toBeNull() + expect(resolveFilterFieldLabel(filterField, 'plain text')).toBeNull() + }) +}) + +describe('getDisplayValue', () => { + it('handles empty, scalar, and object values', () => { + expect(getDisplayValue(null)).toBe('-') + expect(getDisplayValue('hello')).toBe('hello') + expect(getDisplayValue({ a: 1 })).toBe('a: 1') + }) + + it('summarizes name-bearing arrays', () => { + expect( + getDisplayValue([ + { variableName: 'one', variableId: 'v1', value: 1 }, + { variableName: 'two', variableId: 'v2', value: 2 }, + { variableName: 'three', variableId: 'v3', value: 3 }, + ]) + ).toBe('one, two +1') + expect(getDisplayValue(['a', 'b'])).toBe('a, b') + }) +}) diff --git a/apps/sim/lib/workflows/subblocks/display.ts b/apps/sim/lib/workflows/subblocks/display.ts new file mode 100644 index 00000000000..8ddb49f63ae --- /dev/null +++ b/apps/sim/lib/workflows/subblocks/display.ts @@ -0,0 +1,513 @@ +/** + * Pure display helpers for collapsed subblock rows. + * + * Shared by the canvas editor (`workflow-block.tsx`, hook-fed data) and the + * read-only preview (`preview-workflow/.../block.tsx`, prop/store-fed data). + * Every resolver takes plain data instead of hooks so both surfaces run the + * exact same logic and cannot drift. + */ +import { truncate } from '@sim/utils/string' +import type { FilterRule, SortRule } from '@/lib/table/types' +import { DELETED_WORKFLOW_LABEL } from '@/app/workspace/[workspaceId]/logs/utils' +import { getBlock } from '@/blocks' +import type { SubBlockConfig } from '@/blocks/types' + +/** + * Joins display names as "A", "A, B", or "A, B +N". + * Returns null for an empty list so callers can fall through. + */ +export function summarizeNames(names: string[]): string | null { + if (names.length === 0) return null + if (names.length === 1) return names[0] + if (names.length === 2) return `${names[0]}, ${names[1]}` + return `${names[0]}, ${names[1]} +${names.length - 2}` +} + +interface WorkflowTableRow { + id: string + cells: Record +} + +interface FieldFormat { + id: string + name: string + type?: string + value?: string + collapsed?: boolean +} + +interface TagFilterItem { + id: string + tagName: string + fieldType?: string + operator?: string + tagValue: string +} + +interface DocumentTagItem { + id: string + tagName: string + fieldType?: string + value: string +} + +const isTableRowArray = (value: unknown): value is WorkflowTableRow[] => { + if (!Array.isArray(value) || value.length === 0) return false + const firstItem = value[0] + return ( + typeof firstItem === 'object' && + firstItem !== null && + 'id' in firstItem && + 'cells' in firstItem && + typeof firstItem.cells === 'object' + ) +} + +const isFieldFormatArray = (value: unknown): value is FieldFormat[] => { + if (!Array.isArray(value) || value.length === 0) return false + const firstItem = value[0] + return ( + typeof firstItem === 'object' && + firstItem !== null && + 'id' in firstItem && + 'name' in firstItem && + typeof firstItem.name === 'string' + ) +} + +/** Checks if a value is a plain object (not array, not null). */ +const isPlainObject = (value: unknown): value is Record => { + return typeof value === 'object' && value !== null && !Array.isArray(value) +} + +/** Type guard for variable assignments arrays (variables-input subblocks). */ +const isVariableAssignmentsArray = ( + value: unknown +): value is Array<{ id?: string; variableId?: string; variableName?: string; value: unknown }> => { + return ( + Array.isArray(value) && + value.length > 0 && + value.every( + (item) => + typeof item === 'object' && + item !== null && + ('variableName' in item || 'variableId' in item) + ) + ) +} + +const isMessagesArray = (value: unknown): value is Array<{ role: string; content: string }> => { + return ( + Array.isArray(value) && + value.length > 0 && + value.every( + (item) => + typeof item === 'object' && + item !== null && + 'role' in item && + 'content' in item && + typeof item.role === 'string' && + typeof item.content === 'string' + ) + ) +} + +const isTagFilterArray = (value: unknown): value is TagFilterItem[] => { + if (!Array.isArray(value) || value.length === 0) return false + const firstItem = value[0] + return ( + typeof firstItem === 'object' && + firstItem !== null && + 'tagName' in firstItem && + 'tagValue' in firstItem && + typeof firstItem.tagName === 'string' + ) +} + +const isDocumentTagArray = (value: unknown): value is DocumentTagItem[] => { + if (!Array.isArray(value) || value.length === 0) return false + const firstItem = value[0] + return ( + typeof firstItem === 'object' && + firstItem !== null && + 'tagName' in firstItem && + 'value' in firstItem && + !('tagValue' in firstItem) && + typeof firstItem.tagName === 'string' + ) +} + +const isFilterConditionArray = (value: unknown): value is FilterRule[] => { + if (!Array.isArray(value) || value.length === 0) return false + const firstItem = value[0] + return ( + typeof firstItem === 'object' && + firstItem !== null && + 'column' in firstItem && + 'operator' in firstItem && + 'logicalOperator' in firstItem && + typeof firstItem.column === 'string' + ) +} + +const isSortConditionArray = (value: unknown): value is SortRule[] => { + if (!Array.isArray(value) || value.length === 0) return false + const firstItem = value[0] + return ( + typeof firstItem === 'object' && + firstItem !== null && + 'column' in firstItem && + 'direction' in firstItem && + typeof firstItem.column === 'string' && + (firstItem.direction === 'asc' || firstItem.direction === 'desc') + ) +} + +/** + * Attempts to parse a JSON string, returning the parsed value or the + * original value if parsing fails. + */ +const tryParseJson = (value: unknown): unknown => { + if (typeof value !== 'string') return value + try { + const trimmed = value.trim() + if ( + (trimmed.startsWith('[') && trimmed.endsWith(']')) || + (trimmed.startsWith('{') && trimmed.endsWith('}')) + ) { + return JSON.parse(trimmed) + } + } catch { + return value + } + return value +} + +/** + * Formats a subblock value for display, intelligently handling nested + * objects and arrays. + */ +export const getDisplayValue = (value: unknown): string => { + if (value == null || value === '') return '-' + + const parsedValue = tryParseJson(value) + + if (isMessagesArray(parsedValue)) { + const firstMessage = parsedValue[0] + if (!firstMessage?.content || firstMessage.content.trim() === '') return '-' + const content = firstMessage.content.trim() + return truncate(content, 50) + } + + if (isVariableAssignmentsArray(parsedValue)) { + const names = parsedValue.map((a) => a.variableName).filter((name): name is string => !!name) + return summarizeNames(names) ?? '-' + } + + if (isTagFilterArray(parsedValue)) { + const names = parsedValue + .filter((f) => typeof f.tagName === 'string' && f.tagName.trim() !== '') + .map((f) => f.tagName) + return summarizeNames(names) ?? '-' + } + + if (isDocumentTagArray(parsedValue)) { + const names = parsedValue + .filter((t) => typeof t.tagName === 'string' && t.tagName.trim() !== '') + .map((t) => t.tagName) + return summarizeNames(names) ?? '-' + } + + if (isFilterConditionArray(parsedValue)) { + const opLabels: Record = { + eq: '=', + ne: '≠', + gt: '>', + gte: '≥', + lt: '<', + lte: '≤', + contains: '~', + in: 'in', + } + const names = parsedValue + .filter((c) => typeof c.column === 'string' && c.column.trim() !== '') + .map((c) => `${c.column} ${opLabels[c.operator] || c.operator} ${c.value || '?'}`) + return summarizeNames(names) ?? '-' + } + + if (isSortConditionArray(parsedValue)) { + const names = parsedValue + .filter((c) => typeof c.column === 'string' && c.column.trim() !== '') + .map((c) => `${c.column} ${c.direction === 'desc' ? '↓' : '↑'}`) + return summarizeNames(names) ?? '-' + } + + if (isTableRowArray(parsedValue)) { + const nonEmptyRows = parsedValue.filter((row) => { + const cellValues = Object.values(row.cells) + return cellValues.some((cell) => cell && cell.trim() !== '') + }) + + if (nonEmptyRows.length === 0) return '-' + if (nonEmptyRows.length === 1) { + const firstRow = nonEmptyRows[0] + const cellEntries = Object.entries(firstRow.cells).filter(([, val]) => val?.trim()) + if (cellEntries.length === 0) return '-' + const preview = cellEntries + .slice(0, 2) + .map(([key, val]) => `${key}: ${val}`) + .join(', ') + return cellEntries.length > 2 ? `${preview}...` : preview + } + return `${nonEmptyRows.length} rows` + } + + if (isFieldFormatArray(parsedValue)) { + const names = parsedValue + .filter((field) => typeof field.name === 'string' && field.name.trim() !== '') + .map((field) => field.name) + return summarizeNames(names) ?? '-' + } + + if (isPlainObject(parsedValue)) { + const entries = Object.entries(parsedValue).filter( + ([, val]) => val !== null && val !== undefined && val !== '' + ) + + if (entries.length === 0) return '-' + if (entries.length === 1) { + const [key, val] = entries[0] + const valStr = String(val).slice(0, 30) + return `${key}: ${valStr}${String(val).length > 30 ? '...' : ''}` + } + const preview = entries + .slice(0, 2) + .map(([key]) => key) + .join(', ') + return entries.length > 2 ? `${preview} +${entries.length - 2}` : preview + } + + if (Array.isArray(parsedValue)) { + const getItemDisplayValue = (item: unknown): string => { + if (typeof item === 'object' && item !== null) { + const obj = item as Record + return String(obj.title || obj.name || obj.label || obj.id || JSON.stringify(item)) + } + return String(item) + } + const names = parsedValue + .filter((item) => item !== null && item !== undefined && item !== '') + .map(getItemDisplayValue) + return summarizeNames(names) ?? '-' + } + + const stringValue = String(value) + if (stringValue === '[object Object]') { + try { + const json = JSON.stringify(parsedValue) + if (json.length <= 40) return json + return `${json.slice(0, 37)}...` + } catch { + return '-' + } + } + + return stringValue.trim().length > 0 ? stringValue : '-' +} + +/** + * Workflow id -> metadata lookup for the workflow selector resolvers. + * `ready` gates resolution so missing entries only render as deleted once + * the lookup has actually loaded. + */ +interface WorkflowNameLookup { + workflowMap: Record + ready: boolean +} + +/** + * Resolves filter/sort builder subblocks to a compact single-line JSON + * preview. Returns null for other subblocks; callers use a non-null result + * to apply monospace styling. + */ +export function resolveFilterFieldLabel( + subBlock: SubBlockConfig | undefined, + rawValue: unknown +): string | null { + const isFilterField = + subBlock?.id === 'filter' || subBlock?.id === 'filterCriteria' || subBlock?.id === 'sort' + if (!isFilterField || !rawValue) return null + + const parsedValue = tryParseJson(rawValue) + if (!isPlainObject(parsedValue) && !Array.isArray(parsedValue)) return null + + try { + const jsonStr = JSON.stringify(parsedValue, null, 0) + return jsonStr.length <= 35 ? jsonStr : truncate(jsonStr, 32) + } catch { + return null + } +} + +/** + * Resolves a static dropdown/combobox value to its option label. + * Returns null if not a dropdown/combobox or no matching option is found. + */ +export function resolveDropdownLabel( + subBlock: SubBlockConfig | undefined, + rawValue: unknown +): string | null { + if (!subBlock || (subBlock.type !== 'dropdown' && subBlock.type !== 'combobox')) return null + if (!rawValue || typeof rawValue !== 'string') return null + + const options = typeof subBlock.options === 'function' ? subBlock.options() : subBlock.options + if (!options) return null + + const option = options.find((opt) => + typeof opt === 'string' ? opt === rawValue : opt.id === rawValue + ) + + if (!option) return null + return typeof option === 'string' ? option : option.label +} + +/** Resolves a workflow-selector value to the workflow's name. */ +export function resolveWorkflowSelectionLabel( + subBlock: SubBlockConfig | undefined, + rawValue: unknown, + lookup: WorkflowNameLookup +): string | null { + if (subBlock?.type !== 'workflow-selector') return null + if (!rawValue || typeof rawValue !== 'string') return null + if (!lookup.ready) return null + + return lookup.workflowMap[rawValue]?.name ?? DELETED_WORKFLOW_LABEL +} + +/** + * Resolves multi-select workflow dropdowns (e.g. the Sim trigger's workflow + * scope) to a workflow-name summary. + */ +export function resolveWorkflowMultiSelectLabel( + subBlock: SubBlockConfig | undefined, + rawValue: unknown, + lookup: WorkflowNameLookup +): string | null { + const isWorkflowMultiSelect = + subBlock?.type === 'dropdown' && + subBlock.multiSelect && + (subBlock.id === 'workflowIds' || subBlock.canonicalParamId === 'workflowIds') + if (!isWorkflowMultiSelect) return null + if (!Array.isArray(rawValue) || rawValue.length === 0) return null + if (!lookup.ready) return null + + const names = rawValue + .filter((id): id is string => typeof id === 'string' && id.length > 0) + .map((id) => lookup.workflowMap[id]?.name ?? DELETED_WORKFLOW_LABEL) + + return summarizeNames(names) +} + +/** Resolves a variables-input value to a variable-name summary. */ +export function resolveVariablesLabel( + subBlock: SubBlockConfig | undefined, + rawValue: unknown, + variables: Array<{ id: string; name: string }> +): string | null { + if (subBlock?.type !== 'variables-input') return null + if (!isVariableAssignmentsArray(rawValue)) return null + + const names = rawValue + .map((assignment) => { + if (assignment.variableId) { + return variables.find((variable) => variable.id === assignment.variableId)?.name + } + if (assignment.variableName) return assignment.variableName + return null + }) + .filter((name): name is string => !!name) + + return summarizeNames(names) +} + +/** + * Resolves a tool-input value to a tool-name summary. Stored tool entries + * come in several historical shapes, checked in priority order: explicit + * title, custom tool referenced by id, inline schema name, OpenAI function + * name, then the block registry. + */ +export function resolveToolsLabel( + subBlock: SubBlockConfig | undefined, + rawValue: unknown, + customTools: Array<{ id: string; title?: string; schema?: { function?: { name?: string } } }> +): string | null { + if (subBlock?.type !== 'tool-input') return null + if (!Array.isArray(rawValue) || rawValue.length === 0) return null + + const names = rawValue + .map((tool: unknown) => { + if (!tool || typeof tool !== 'object') return null + const t = tool as Record + + if (typeof t.title === 'string' && t.title) return t.title + + if (t.type === 'custom-tool' && typeof t.customToolId === 'string') { + const customTool = customTools.find((candidate) => candidate.id === t.customToolId) + if (customTool?.title) return customTool.title + if (customTool?.schema?.function?.name) return customTool.schema.function.name + } + + const schema = t.schema as { function?: { name?: string } } | undefined + if (schema?.function?.name) return schema.function.name + + const fn = t.function as { name?: string } | undefined + if (fn?.name) return fn.name + + if ( + typeof t.type === 'string' && + t.type !== 'custom-tool' && + t.type !== 'mcp' && + t.type !== 'workflow' && + t.type !== 'workflow_input' + ) { + const blockConfig = getBlock(t.type) + if (blockConfig?.name) return blockConfig.name + } + + return null + }) + .filter((name): name is string => !!name) + + return summarizeNames(names) +} + +/** + * Resolves a skill-input value to a skill-name summary: the live skill name + * when the skill still exists, otherwise the name stored alongside the + * reference. Unresolvable entries are skipped rather than shown as raw ids. + */ +export function resolveSkillsLabel( + subBlock: SubBlockConfig | undefined, + rawValue: unknown, + skills: Array<{ id: string; name: string }> +): string | null { + if (subBlock?.type !== 'skill-input') return null + if (!Array.isArray(rawValue) || rawValue.length === 0) return null + + const names = rawValue + .map((skill: unknown) => { + if (!skill || typeof skill !== 'object') return null + const s = skill as { skillId?: string; name?: string } + + if (s.skillId) { + const found = skills.find((candidate) => candidate.id === s.skillId) + if (found?.name) return found.name + } + if (typeof s.name === 'string' && s.name) return s.name + + return null + }) + .filter((name): name is string => !!name) + + return summarizeNames(names) +} diff --git a/apps/sim/lib/workflows/subblocks/options.ts b/apps/sim/lib/workflows/subblocks/options.ts new file mode 100644 index 00000000000..526dcb0d826 --- /dev/null +++ b/apps/sim/lib/workflows/subblocks/options.ts @@ -0,0 +1,32 @@ +import { getQueryClient } from '@/app/_shell/providers/get-query-client' +import { getWorkflowListQueryOptions } from '@/hooks/queries/utils/workflow-list-query' +import { useWorkflowRegistry } from '@/stores/workflows/registry/store' + +interface SubBlockOption { + label: string + id: string +} + +/** + * Loads the active workspace's workflows for multi-select subblocks + * (`fetchOptions`). Set `excludeActiveWorkflow` for surfaces where selecting + * the current workflow is meaningless (e.g. the Sim trigger never receives + * events about itself). + */ +export async function fetchWorkspaceWorkflowOptions(options?: { + excludeActiveWorkflow?: boolean +}): Promise { + const registry = useWorkflowRegistry.getState() + const workspaceId = registry.hydration.workspaceId + if (!workspaceId) return [] + + const workflows = await getQueryClient().fetchQuery( + getWorkflowListQueryOptions(workspaceId, 'active') + ) + + return workflows + .filter( + (workflow) => !options?.excludeActiveWorkflow || workflow.id !== registry.activeWorkflowId + ) + .map((workflow) => ({ id: workflow.id, label: workflow.name })) +} diff --git a/apps/sim/lib/workflows/triggers/triggers.ts b/apps/sim/lib/workflows/triggers/triggers.ts index ab5de09c219..a7b5077afcb 100644 --- a/apps/sim/lib/workflows/triggers/triggers.ts +++ b/apps/sim/lib/workflows/triggers/triggers.ts @@ -12,6 +12,7 @@ export const TRIGGER_TYPES = { WEBHOOK: 'webhook', GENERIC_WEBHOOK: 'generic_webhook', SCHEDULE: 'schedule', + SIM: 'sim_workspace_event', START: 'start_trigger', STARTER: 'starter', // Legacy } as const @@ -96,6 +97,7 @@ export function classifyStartBlockType( return StartBlockPath.SPLIT_MANUAL case TRIGGER_TYPES.WEBHOOK: case TRIGGER_TYPES.SCHEDULE: + case TRIGGER_TYPES.SIM: return StartBlockPath.EXTERNAL_TRIGGER default: if (opts?.category === 'triggers' || opts?.triggerModeEnabled) { diff --git a/apps/sim/lib/workspace-events/constants.ts b/apps/sim/lib/workspace-events/constants.ts new file mode 100644 index 00000000000..5a5c0f09b8e --- /dev/null +++ b/apps/sim/lib/workspace-events/constants.ts @@ -0,0 +1,169 @@ +/** + * Shared constants for the Sim workspace-event trigger. + * + * This module is imported from both client code (trigger/block definitions) + * and server code (the event emitter), so it must stay free of server-only + * dependencies such as the database client. + */ + +/** Provider string recorded on webhook rows and execution logs for Sim trigger runs. */ +export const SIM_TRIGGER_PROVIDER = 'sim' + +/** Trigger ID in the trigger registry. Must equal the block type for pure trigger blocks. */ +export const SIM_WORKSPACE_EVENT_TRIGGER_ID = 'sim_workspace_event' + +/** Events that fire 1:1 with their source occurrence (no rule evaluation, no cooldown). */ +export const SIM_PLAIN_EVENT_TYPES = [ + 'execution_success', + 'execution_error', + 'workflow_deployed', +] as const + +/** Rule-based events ported from the legacy notification alert rules. */ +export const SIM_RULE_EVENT_TYPES = [ + 'consecutive_failures', + 'failure_rate', + 'latency_threshold', + 'latency_spike', + 'cost_threshold', + 'error_count', + 'no_activity', +] as const + +export const SIM_EVENT_TYPES = [...SIM_PLAIN_EVENT_TYPES, ...SIM_RULE_EVENT_TYPES] as const + +export type SimPlainEventType = (typeof SIM_PLAIN_EVENT_TYPES)[number] +export type SimRuleEventType = (typeof SIM_RULE_EVENT_TYPES)[number] +export type SimEventType = (typeof SIM_EVENT_TYPES)[number] + +/** + * Plain events that ARE a run completing. These carry the run summary fields + * (runId, durationMs, cost, finalOutput) at the top level. + */ +const SIM_PLAIN_RUN_EVENT_TYPES = ['execution_success', 'execution_error'] as const + +/** + * Rule events tripped by a run completing. The run is evidence for the + * condition rather than the event itself, so its summary nests under + * `triggeringRun`. no_activity is excluded — it has no triggering run. + */ +const SIM_RUN_BACKED_RULE_EVENT_TYPES = SIM_RULE_EVENT_TYPES.filter( + (eventType) => eventType !== 'no_activity' +) + +export function isSimRuleEventType(eventType: string): eventType is SimRuleEventType { + return (SIM_RULE_EVENT_TYPES as readonly string[]).includes(eventType) +} + +/** Cooldown between firings of the same rule-based subscription. */ +export const SIM_RULE_COOLDOWN_HOURS = 1 + +/** Minimum executions in the window before rate-based rules can fire. */ +export const SIM_MIN_EXECUTIONS_FOR_RATE_RULES = 5 + +/** Default values for rule configuration subblocks, ported from the legacy alert rules. */ +export const SIM_RULE_DEFAULTS = { + consecutiveFailures: 3, + failureRatePercent: 50, + windowHours: 24, + durationThresholdMs: 30000, + latencySpikePercent: 100, + /** 200 credits = $1 (1 credit = $0.005). */ + costThresholdCredits: 200, + errorCountThreshold: 10, + inactivityHours: 24, +} as const + +/** Maximum serialized size of the finalOutput payload field. */ +export const SIM_FINAL_OUTPUT_MAX_BYTES = 64 * 1024 + +interface SimEventPayloadFieldCondition { + field: 'eventType' + value: SimEventType | SimEventType[] +} + +interface SimEventPayloadField { + type: 'string' | 'number' | 'json' | 'boolean' + description: string + /** Restricts which event types surface this field in the tag dropdown. */ + condition?: SimEventPayloadFieldCondition + /** Nested fields for json outputs, surfaced as dotted paths in the tag dropdown. */ + properties?: Record +} + +/** Run summary fields shared by top-level plain events and the nested triggeringRun. */ +const RUN_SUMMARY_FIELDS = { + runId: { + type: 'string', + description: 'The source run ID', + }, + durationMs: { + type: 'number', + description: 'Source run duration in milliseconds', + }, + cost: { + type: 'number', + description: 'Source run cost in credits', + }, + finalOutput: { + type: 'json', + description: 'Final output of the source run (truncated when large)', + }, +} as const + +/** + * Canonical payload shape delivered to Sim trigger workflows. + * + * The trigger's declared outputs and the runtime payload builder both derive + * from this map so the tag dropdown and the actual payload can never drift + * (enforced by tests on both sides). Conditions narrow the tag dropdown to + * the fields that are meaningful for the selected event type; the runtime + * payload always carries every key (null where not applicable). + */ +export const SIM_EVENT_PAYLOAD_FIELDS = { + event: { + type: 'string', + description: 'The workspace event type that fired this trigger', + }, + timestamp: { + type: 'string', + description: 'Event timestamp in ISO format', + }, + workflowId: { + type: 'string', + description: 'The source workflow ID', + }, + workflowName: { + type: 'string', + description: 'The source workflow name', + }, + runId: { + ...RUN_SUMMARY_FIELDS.runId, + condition: { field: 'eventType', value: [...SIM_PLAIN_RUN_EVENT_TYPES] }, + }, + durationMs: { + ...RUN_SUMMARY_FIELDS.durationMs, + condition: { field: 'eventType', value: [...SIM_PLAIN_RUN_EVENT_TYPES] }, + }, + cost: { + ...RUN_SUMMARY_FIELDS.cost, + condition: { field: 'eventType', value: [...SIM_PLAIN_RUN_EVENT_TYPES] }, + }, + finalOutput: { + ...RUN_SUMMARY_FIELDS.finalOutput, + condition: { field: 'eventType', value: [...SIM_PLAIN_RUN_EVENT_TYPES] }, + }, + triggeringRun: { + type: 'json', + description: 'The run that tripped this condition', + condition: { field: 'eventType', value: [...SIM_RUN_BACKED_RULE_EVENT_TYPES] }, + properties: RUN_SUMMARY_FIELDS, + }, + version: { + type: 'number', + description: 'The deployment version number that was activated', + condition: { field: 'eventType', value: 'workflow_deployed' }, + }, +} as const satisfies Record + +export type SimEventPayloadFieldKey = keyof typeof SIM_EVENT_PAYLOAD_FIELDS diff --git a/apps/sim/lib/workspace-events/emitter.test.ts b/apps/sim/lib/workspace-events/emitter.test.ts new file mode 100644 index 00000000000..9fc893ee6a2 --- /dev/null +++ b/apps/sim/lib/workspace-events/emitter.test.ts @@ -0,0 +1,386 @@ +/** + * @vitest-environment node + */ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { + mockGetActiveWorkflowContext, + mockFetchSubscriptions, + mockEvaluateRule, + mockReadLastFiredAt, + mockClaimCooldown, + mockProcessPolledWebhookEvent, +} = vi.hoisted(() => ({ + mockGetActiveWorkflowContext: vi.fn(), + mockFetchSubscriptions: vi.fn(), + mockEvaluateRule: vi.fn(), + mockReadLastFiredAt: vi.fn(), + mockClaimCooldown: vi.fn(), + mockProcessPolledWebhookEvent: vi.fn(), +})) + +vi.mock('@sim/workflow-authz', () => ({ + getActiveWorkflowContext: mockGetActiveWorkflowContext, +})) + +vi.mock('@/lib/workspace-events/subscriptions', () => ({ + fetchSimTriggerSubscriptions: mockFetchSubscriptions, + parseSubscriptionConfig: vi.fn((providerConfig: unknown) => providerConfig), +})) + +vi.mock('@/lib/workspace-events/rules', () => ({ + evaluateRule: mockEvaluateRule, +})) + +vi.mock('@/lib/workspace-events/state', () => ({ + readLastFiredAt: mockReadLastFiredAt, + claimCooldown: mockClaimCooldown, + isWithinCooldown: vi.fn( + (lastFiredAt: Date | null, cooldownMs: number) => + lastFiredAt !== null && Date.now() - lastFiredAt.getTime() < cooldownMs + ), +})) + +vi.mock('@/lib/webhooks/processor', () => ({ + processPolledWebhookEvent: mockProcessPolledWebhookEvent, +})) + +import type { WorkflowExecutionLog } from '@/lib/logs/types' +import { + emitExecutionCompletedEvent, + emitWorkflowDeployedEvent, +} from '@/lib/workspace-events/emitter' +import type { SimSubscriptionConfig } from '@/lib/workspace-events/types' + +function makeConfig(overrides: Partial = {}): SimSubscriptionConfig { + return { + eventType: 'execution_error', + workflowIds: [], + consecutiveFailures: 3, + failureRatePercent: 50, + windowHours: 24, + durationThresholdMs: 30000, + latencySpikePercent: 100, + costThresholdCredits: 200, + errorCountThreshold: 10, + inactivityHours: 24, + ...overrides, + } +} + +function makeSubscription( + config: SimSubscriptionConfig, + overrides: { subscriberWorkflowId?: string; blockId?: string } = {} +) { + const subscriberWorkflowId = overrides.subscriberWorkflowId ?? 'wf-subscriber' + return { + webhook: { + id: `wh-${subscriberWorkflowId}`, + workflowId: subscriberWorkflowId, + blockId: overrides.blockId ?? 'block-1', + path: 'block-1', + provider: 'sim', + providerConfig: config, + isActive: true, + }, + workflow: { + id: subscriberWorkflowId, + name: 'Subscriber Workflow', + }, + } +} + +function makeLog(overrides: Partial = {}): WorkflowExecutionLog { + return { + id: 'log-1', + workflowId: 'wf-source', + executionId: 'exec-1', + stateSnapshotId: 'snap-1', + level: 'error', + trigger: 'manual', + startedAt: '2026-06-09T00:00:00.000Z', + endedAt: '2026-06-09T00:00:01.000Z', + totalDurationMs: 1000, + executionData: { + error: 'boom', + finalOutput: { result: 42 }, + } as WorkflowExecutionLog['executionData'], + cost: { total: 0.25 } as WorkflowExecutionLog['cost'], + createdAt: '2026-06-09T00:00:01.000Z', + ...overrides, + } +} + +describe('emitExecutionCompletedEvent', () => { + beforeEach(() => { + vi.clearAllMocks() + mockGetActiveWorkflowContext.mockResolvedValue({ + workflow: { id: 'wf-source', name: 'Source Workflow' }, + workspaceId: 'ws-1', + }) + mockFetchSubscriptions.mockResolvedValue([]) + mockProcessPolledWebhookEvent.mockResolvedValue({ success: true, executionId: 'exec-2' }) + mockReadLastFiredAt.mockResolvedValue(null) + mockClaimCooldown.mockResolvedValue(true) + mockEvaluateRule.mockResolvedValue(true) + }) + + it('never emits for executions started by the sim trigger (loop guard)', async () => { + await emitExecutionCompletedEvent(makeLog({ trigger: 'sim' })) + + expect(mockGetActiveWorkflowContext).not.toHaveBeenCalled() + expect(mockFetchSubscriptions).not.toHaveBeenCalled() + expect(mockProcessPolledWebhookEvent).not.toHaveBeenCalled() + }) + + it('does nothing without a workflow id or workspace context', async () => { + await emitExecutionCompletedEvent(makeLog({ workflowId: null })) + expect(mockFetchSubscriptions).not.toHaveBeenCalled() + + mockGetActiveWorkflowContext.mockResolvedValueOnce(null) + await emitExecutionCompletedEvent(makeLog()) + expect(mockFetchSubscriptions).not.toHaveBeenCalled() + }) + + it('looks up subscriptions scoped to the source workspace', async () => { + await emitExecutionCompletedEvent(makeLog()) + expect(mockFetchSubscriptions).toHaveBeenCalledWith('ws-1') + }) + + it('fires execution_error subscribers for error logs but not execution_success ones', async () => { + const errorSub = makeSubscription(makeConfig({ eventType: 'execution_error' }), { + subscriberWorkflowId: 'wf-error-sub', + }) + const successSub = makeSubscription(makeConfig({ eventType: 'execution_success' }), { + subscriberWorkflowId: 'wf-success-sub', + }) + mockFetchSubscriptions.mockResolvedValueOnce([errorSub, successSub]) + + await emitExecutionCompletedEvent(makeLog({ level: 'error' })) + + expect(mockProcessPolledWebhookEvent).toHaveBeenCalledTimes(1) + expect(mockProcessPolledWebhookEvent).toHaveBeenCalledWith( + errorSub.webhook, + errorSub.workflow, + expect.objectContaining({ + event: 'execution_error', + workflowId: 'wf-source', + workflowName: 'Source Workflow', + runId: 'exec-1', + durationMs: 1000, + // $0.25 reported as credits (1 credit = $0.005) + cost: 50, + }), + expect.any(String) + ) + }) + + it('fires execution_success subscribers for info logs', async () => { + const successSub = makeSubscription(makeConfig({ eventType: 'execution_success' })) + mockFetchSubscriptions.mockResolvedValueOnce([successSub]) + + await emitExecutionCompletedEvent(makeLog({ level: 'info' })) + + expect(mockProcessPolledWebhookEvent).toHaveBeenCalledTimes(1) + expect(mockProcessPolledWebhookEvent.mock.calls[0][2]).toMatchObject({ + event: 'execution_success', + runId: 'exec-1', + }) + }) + + it('respects the workflow scope filter, ignoring stale workflow ids', async () => { + const matching = makeSubscription(makeConfig({ workflowIds: ['wf-source', 'wf-deleted'] }), { + subscriberWorkflowId: 'wf-a', + }) + const nonMatching = makeSubscription(makeConfig({ workflowIds: ['wf-other', 'wf-deleted'] }), { + subscriberWorkflowId: 'wf-b', + }) + mockFetchSubscriptions.mockResolvedValueOnce([matching, nonMatching]) + + await emitExecutionCompletedEvent(makeLog()) + + expect(mockProcessPolledWebhookEvent).toHaveBeenCalledTimes(1) + expect(mockProcessPolledWebhookEvent.mock.calls[0][0]).toBe(matching.webhook) + }) + + it('an empty workflow selection watches every workflow', async () => { + const watchAll = makeSubscription(makeConfig({ workflowIds: [] })) + mockFetchSubscriptions.mockResolvedValueOnce([watchAll]) + + await emitExecutionCompletedEvent(makeLog()) + + expect(mockProcessPolledWebhookEvent).toHaveBeenCalledTimes(1) + }) + + it('never fires a subscription for its own workflow, even when watching all workflows', async () => { + const selfSub = makeSubscription(makeConfig({ workflowIds: [] }), { + subscriberWorkflowId: 'wf-source', + }) + mockFetchSubscriptions.mockResolvedValueOnce([selfSub]) + + await emitExecutionCompletedEvent(makeLog()) + + expect(mockProcessPolledWebhookEvent).not.toHaveBeenCalled() + }) + + it('plain events bypass cooldown state entirely', async () => { + mockFetchSubscriptions.mockResolvedValueOnce([ + makeSubscription(makeConfig({ eventType: 'execution_error' })), + ]) + + await emitExecutionCompletedEvent(makeLog()) + + expect(mockProcessPolledWebhookEvent).toHaveBeenCalledTimes(1) + expect(mockReadLastFiredAt).not.toHaveBeenCalled() + expect(mockClaimCooldown).not.toHaveBeenCalled() + }) + + it('rule events evaluate the rule and claim the cooldown before dispatching', async () => { + const sub = makeSubscription(makeConfig({ eventType: 'cost_threshold' })) + mockFetchSubscriptions.mockResolvedValueOnce([sub]) + + await emitExecutionCompletedEvent(makeLog()) + + expect(mockEvaluateRule).toHaveBeenCalledTimes(1) + expect(mockClaimCooldown).toHaveBeenCalledWith( + 'wf-subscriber', + 'block-1', + '', + expect.any(Number) + ) + expect(mockProcessPolledWebhookEvent).toHaveBeenCalledTimes(1) + expect(mockProcessPolledWebhookEvent.mock.calls[0][2]).toMatchObject({ + event: 'cost_threshold', + runId: null, + triggeringRun: { runId: 'exec-1' }, + }) + }) + + it('skips no_activity subscriptions before any cooldown read or rule evaluation (poller-owned)', async () => { + const sub = makeSubscription(makeConfig({ eventType: 'no_activity' })) + mockFetchSubscriptions.mockResolvedValueOnce([sub]) + + await emitExecutionCompletedEvent(makeLog()) + + expect(mockReadLastFiredAt).not.toHaveBeenCalled() + expect(mockEvaluateRule).not.toHaveBeenCalled() + expect(mockClaimCooldown).not.toHaveBeenCalled() + expect(mockProcessPolledWebhookEvent).not.toHaveBeenCalled() + }) + + it('skips rule evaluation while within the cooldown window', async () => { + mockReadLastFiredAt.mockResolvedValueOnce(new Date()) + mockFetchSubscriptions.mockResolvedValueOnce([ + makeSubscription(makeConfig({ eventType: 'latency_threshold' })), + ]) + + await emitExecutionCompletedEvent(makeLog()) + + expect(mockEvaluateRule).not.toHaveBeenCalled() + expect(mockProcessPolledWebhookEvent).not.toHaveBeenCalled() + }) + + it('does not dispatch when the rule does not fire', async () => { + mockEvaluateRule.mockResolvedValueOnce(false) + mockFetchSubscriptions.mockResolvedValueOnce([ + makeSubscription(makeConfig({ eventType: 'consecutive_failures' })), + ]) + + await emitExecutionCompletedEvent(makeLog()) + + expect(mockClaimCooldown).not.toHaveBeenCalled() + expect(mockProcessPolledWebhookEvent).not.toHaveBeenCalled() + }) + + it('does not dispatch when a concurrent emitter wins the cooldown claim', async () => { + mockClaimCooldown.mockResolvedValueOnce(false) + mockFetchSubscriptions.mockResolvedValueOnce([ + makeSubscription(makeConfig({ eventType: 'error_count' })), + ]) + + await emitExecutionCompletedEvent(makeLog()) + + expect(mockProcessPolledWebhookEvent).not.toHaveBeenCalled() + }) + + it('always includes the source execution finalOutput', async () => { + mockFetchSubscriptions.mockResolvedValueOnce([makeSubscription(makeConfig())]) + + await emitExecutionCompletedEvent(makeLog()) + + expect(mockProcessPolledWebhookEvent).toHaveBeenCalledTimes(1) + expect(mockProcessPolledWebhookEvent.mock.calls[0][2]).toMatchObject({ + finalOutput: { result: 42 }, + }) + }) + + it('never throws when emission internals fail', async () => { + mockFetchSubscriptions.mockRejectedValueOnce(new Error('db down')) + await expect(emitExecutionCompletedEvent(makeLog())).resolves.toBeUndefined() + + mockProcessPolledWebhookEvent.mockRejectedValueOnce(new Error('enqueue failed')) + mockFetchSubscriptions.mockResolvedValueOnce([makeSubscription(makeConfig())]) + await expect(emitExecutionCompletedEvent(makeLog())).resolves.toBeUndefined() + }) +}) + +describe('emitWorkflowDeployedEvent', () => { + beforeEach(() => { + vi.clearAllMocks() + mockFetchSubscriptions.mockResolvedValue([]) + mockProcessPolledWebhookEvent.mockResolvedValue({ success: true, executionId: 'exec-2' }) + }) + + const deployParams = { + workflowId: 'wf-source', + workflowName: 'Source Workflow', + workspaceId: 'ws-1', + version: 4, + } + + it('fires only workflow_deployed subscribers on deploys', async () => { + const deploySub = makeSubscription(makeConfig({ eventType: 'workflow_deployed' })) + const errorSub = makeSubscription(makeConfig({ eventType: 'execution_error' }), { + subscriberWorkflowId: 'wf-other', + }) + mockFetchSubscriptions.mockResolvedValueOnce([deploySub, errorSub]) + + await emitWorkflowDeployedEvent(deployParams) + + expect(mockProcessPolledWebhookEvent).toHaveBeenCalledTimes(1) + expect(mockProcessPolledWebhookEvent.mock.calls[0][2]).toMatchObject({ + event: 'workflow_deployed', + workflowId: 'wf-source', + workflowName: 'Source Workflow', + runId: null, + version: 4, + }) + }) + + it('does not fire a subscription when its own workflow is deployed', async () => { + const selfSub = makeSubscription(makeConfig({ eventType: 'workflow_deployed' }), { + subscriberWorkflowId: 'wf-source', + }) + mockFetchSubscriptions.mockResolvedValueOnce([selfSub]) + + await emitWorkflowDeployedEvent(deployParams) + + expect(mockProcessPolledWebhookEvent).not.toHaveBeenCalled() + }) + + it('respects the workflow scope filter', async () => { + const outOfScope = makeSubscription( + makeConfig({ eventType: 'workflow_deployed', workflowIds: ['wf-x'] }) + ) + mockFetchSubscriptions.mockResolvedValueOnce([outOfScope]) + + await emitWorkflowDeployedEvent(deployParams) + + expect(mockProcessPolledWebhookEvent).not.toHaveBeenCalled() + }) + + it('never throws when emission internals fail', async () => { + mockFetchSubscriptions.mockRejectedValueOnce(new Error('db down')) + await expect(emitWorkflowDeployedEvent(deployParams)).resolves.toBeUndefined() + }) +}) diff --git a/apps/sim/lib/workspace-events/emitter.ts b/apps/sim/lib/workspace-events/emitter.ts new file mode 100644 index 00000000000..07e3ddaa2f2 --- /dev/null +++ b/apps/sim/lib/workspace-events/emitter.ts @@ -0,0 +1,198 @@ +import { createLogger } from '@sim/logger' +import { generateShortId } from '@sim/utils/id' +import { getActiveWorkflowContext } from '@sim/workflow-authz' +import type { WorkflowExecutionLog } from '@/lib/logs/types' +import { + isSimRuleEventType, + SIM_RULE_COOLDOWN_HOURS, + SIM_TRIGGER_PROVIDER, +} from '@/lib/workspace-events/constants' +import { buildDeployEventPayload, buildExecutionEventPayload } from '@/lib/workspace-events/payload' +import { evaluateRule } from '@/lib/workspace-events/rules' +import { claimCooldown, isWithinCooldown, readLastFiredAt } from '@/lib/workspace-events/state' +import { + fetchSimTriggerSubscriptions, + parseSubscriptionConfig, +} from '@/lib/workspace-events/subscriptions' +import type { + ExecutionEventContext, + SimEventPayload, + SimSubscription, + SimSubscriptionConfig, +} from '@/lib/workspace-events/types' + +const logger = createLogger('WorkspaceEventEmitter') + +const SIM_RULE_COOLDOWN_MS = SIM_RULE_COOLDOWN_HOURS * 60 * 60 * 1000 + +/** Stable cooldown identity for a subscriber block, surviving redeploys. */ +function subscriptionBlockKey(subscription: SimSubscription): string { + return subscription.webhook.blockId ?? subscription.webhook.path +} + +/** + * Enqueues one side-effect workflow execution for a matched subscription. + * + * Routes through the shared polled-webhook pipeline, which provides admission + * control, billing attribution, deployment checks, and queue-vs-inline + * routing. The processor stack (executor, blocks) is imported lazily so this + * module stays cheap for the execution logger to import. + */ +export async function dispatchSimEvent( + subscription: SimSubscription, + payload: SimEventPayload +): Promise { + const requestId = generateShortId() + try { + const { processPolledWebhookEvent } = await import('@/lib/webhooks/processor') + const result = await processPolledWebhookEvent( + subscription.webhook, + subscription.workflow, + payload, + requestId + ) + + if (!result.success) { + logger.error( + `[${requestId}] Failed to fire sim trigger for workflow ${subscription.workflow.id}:`, + result.statusCode, + result.error + ) + } + } catch (error) { + logger.error( + `[${requestId}] Error firing sim trigger for workflow ${subscription.workflow.id}:`, + error + ) + } +} + +/** Workflow-scope filter shared by all event kinds. Empty selection watches every workflow. */ +function matchesWorkflowScope(config: SimSubscriptionConfig, sourceWorkflowId: string): boolean { + if (config.workflowIds.length === 0) return true + return config.workflowIds.includes(sourceWorkflowId) +} + +/** + * Emits workspace events for a completed workflow execution. + * + * Fire-and-forget: errors are logged and never thrown, so event emission can + * never break the source execution. Executions started by the Sim trigger + * itself never emit (loop prevention). + */ +export async function emitExecutionCompletedEvent(log: WorkflowExecutionLog): Promise { + try { + if (!log.workflowId) return + if (log.trigger === SIM_TRIGGER_PROVIDER) return + + const workflowContext = await getActiveWorkflowContext(log.workflowId) + if (!workflowContext?.workspaceId) return + + const subscriptions = await fetchSimTriggerSubscriptions(workflowContext.workspaceId) + if (subscriptions.length === 0) return + + const executionData = (log.executionData ?? {}) as Record + const context: ExecutionEventContext = { + workflowId: log.workflowId, + executionId: log.executionId, + status: log.level === 'error' ? 'error' : 'success', + durationMs: log.totalDurationMs || 0, + cost: (log.cost as { total?: number } | undefined)?.total || 0, + finalOutput: executionData.finalOutput, + } + + for (const subscription of subscriptions) { + const config = parseSubscriptionConfig(subscription.webhook.providerConfig) + if (!config) continue + if (config.eventType === 'workflow_deployed') continue + // no_activity is owned by the inactivity poller and can never fire from + // a completed execution; skip before the rule branch costs a cooldown + // read on this hot path. + if (config.eventType === 'no_activity') continue + + if (subscription.webhook.workflowId === log.workflowId) continue + if (!matchesWorkflowScope(config, log.workflowId)) continue + + if (config.eventType === 'execution_success' && context.status !== 'success') continue + if (config.eventType === 'execution_error' && context.status !== 'error') continue + + if (isSimRuleEventType(config.eventType)) { + const blockKey = subscriptionBlockKey(subscription) + + const lastFiredAt = await readLastFiredAt(subscription.webhook.workflowId, blockKey, '') + if (isWithinCooldown(lastFiredAt, SIM_RULE_COOLDOWN_MS)) continue + + const ruleFired = await evaluateRule(config.eventType, config, context) + if (!ruleFired) continue + + const claimed = await claimCooldown( + subscription.webhook.workflowId, + blockKey, + '', + SIM_RULE_COOLDOWN_MS + ) + if (!claimed) continue + + logger.info(`Sim trigger rule ${config.eventType} fired`, { + subscriberWorkflowId: subscription.webhook.workflowId, + sourceWorkflowId: log.workflowId, + executionId: log.executionId, + }) + } + + const payload = buildExecutionEventPayload({ + event: config.eventType as Parameters[0]['event'], + workflowName: workflowContext.workflow.name, + context, + }) + + await dispatchSimEvent(subscription, payload) + } + } catch (error) { + logger.error('Failed to emit workspace execution event', { + error, + workflowId: log.workflowId, + executionId: log.executionId, + }) + } +} + +/** + * Emits a workflow_deployed event to subscribed side-effect workflows. + * + * Fired on any deployment activation (fresh deploy, redeploy, version + * rollback/activation). Fire-and-forget: failures never affect the deploy. + */ +export async function emitWorkflowDeployedEvent(params: { + workflowId: string + workflowName: string + workspaceId: string + version: number | null +}): Promise { + try { + const subscriptions = await fetchSimTriggerSubscriptions(params.workspaceId) + if (subscriptions.length === 0) return + + for (const subscription of subscriptions) { + const config = parseSubscriptionConfig(subscription.webhook.providerConfig) + if (!config) continue + if (config.eventType !== 'workflow_deployed') continue + + if (subscription.webhook.workflowId === params.workflowId) continue + if (!matchesWorkflowScope(config, params.workflowId)) continue + + const payload = buildDeployEventPayload({ + workflowId: params.workflowId, + workflowName: params.workflowName, + version: params.version, + }) + + await dispatchSimEvent(subscription, payload) + } + } catch (error) { + logger.error('Failed to emit workflow deployed event', { + error, + workflowId: params.workflowId, + }) + } +} diff --git a/apps/sim/lib/workspace-events/no-activity.test.ts b/apps/sim/lib/workspace-events/no-activity.test.ts new file mode 100644 index 00000000000..bee1fec1bf2 --- /dev/null +++ b/apps/sim/lib/workspace-events/no-activity.test.ts @@ -0,0 +1,286 @@ +/** + * @vitest-environment node + */ +import { dbChainMock, dbChainMockFns } from '@sim/testing' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { mockDispatchSimEvent, mockReadLastFiredAt, mockClaimCooldown } = vi.hoisted(() => ({ + mockDispatchSimEvent: vi.fn(), + mockReadLastFiredAt: vi.fn(), + mockClaimCooldown: vi.fn(), +})) + +vi.mock('@sim/db', () => dbChainMock) + +vi.mock('@/lib/workspace-events/emitter', () => ({ + dispatchSimEvent: mockDispatchSimEvent, +})) + +vi.mock('@/lib/workspace-events/state', () => ({ + readLastFiredAt: mockReadLastFiredAt, + claimCooldown: mockClaimCooldown, + isWithinCooldown: vi.fn( + (lastFiredAt: Date | null, cooldownMs: number) => + lastFiredAt !== null && Date.now() - lastFiredAt.getTime() < cooldownMs + ), +})) + +vi.mock('@/lib/workspace-events/subscriptions', () => ({ + parseSubscriptionConfig: vi.fn((providerConfig: unknown) => providerConfig), +})) + +vi.mock('@/lib/workspace-events/rules', () => ({ + excludeSimExecutionsCondition: vi.fn(() => ({ type: 'ne', right: 'sim' })), +})) + +import { + NO_ACTIVITY_SUBSCRIPTION_PAGE_SIZE, + NO_ACTIVITY_WORKFLOW_PAGE_SIZE, + pollNoActivityEvents, +} from '@/lib/workspace-events/no-activity' +import type { SimSubscriptionConfig } from '@/lib/workspace-events/types' + +function makeConfig(overrides: Partial = {}): SimSubscriptionConfig { + return { + eventType: 'no_activity', + workflowIds: [], + consecutiveFailures: 3, + failureRatePercent: 50, + windowHours: 24, + durationThresholdMs: 30000, + latencySpikePercent: 100, + costThresholdCredits: 200, + errorCountThreshold: 10, + inactivityHours: 24, + ...overrides, + } +} + +/** Flattens nested and/or condition trees from the drizzle operator mocks. */ +function flattenCondition(condition: unknown): unknown[] { + if (!condition || typeof condition !== 'object') return [] + const node = condition as { type?: string; conditions?: unknown[] } + if (node.type === 'and' || node.type === 'or') { + return [node, ...(node.conditions ?? []).flatMap(flattenCondition)] + } + return [node] +} + +function allWhereConditions(): unknown[] { + return dbChainMockFns.where.mock.calls.flatMap(([condition]) => flattenCondition(condition)) +} + +function makeSubscriptionRow(config: SimSubscriptionConfig, webhookId = 'wh-1') { + return { + webhook: { + id: webhookId, + workflowId: 'wf-subscriber', + blockId: 'block-1', + path: 'block-1', + provider: 'sim', + providerConfig: config, + isActive: true, + }, + workflow: { + id: 'wf-subscriber', + name: 'Subscriber', + workspaceId: 'ws-1', + }, + } +} + +describe('pollNoActivityEvents', () => { + beforeEach(() => { + vi.clearAllMocks() + mockReadLastFiredAt.mockResolvedValue(null) + mockClaimCooldown.mockResolvedValue(true) + mockDispatchSimEvent.mockResolvedValue(undefined) + }) + + it('does nothing when there are no no_activity subscriptions', async () => { + dbChainMockFns.limit.mockResolvedValueOnce([]) + + const result = await pollNoActivityEvents() + + expect(result).toEqual({ subscriptions: 0, checked: 0, fired: 0, skipped: 0 }) + expect(mockDispatchSimEvent).not.toHaveBeenCalled() + }) + + it('fires for a watched workflow with no executions in the window', async () => { + dbChainMockFns.limit + .mockResolvedValueOnce([makeSubscriptionRow(makeConfig())]) + .mockResolvedValueOnce([{ id: 'wf-quiet', name: 'Quiet Workflow' }]) + .mockResolvedValueOnce([]) + + const result = await pollNoActivityEvents() + + expect(result.fired).toBe(1) + expect(mockClaimCooldown).toHaveBeenCalledWith( + 'wf-subscriber', + 'block-1', + 'wf-quiet', + expect.any(Number) + ) + expect(mockDispatchSimEvent).toHaveBeenCalledTimes(1) + expect(mockDispatchSimEvent.mock.calls[0][1]).toMatchObject({ + event: 'no_activity', + workflowId: 'wf-quiet', + workflowName: 'Quiet Workflow', + runId: null, + }) + }) + + it('does not fire for a workflow with recent activity', async () => { + dbChainMockFns.limit + .mockResolvedValueOnce([makeSubscriptionRow(makeConfig())]) + .mockResolvedValueOnce([{ id: 'wf-busy', name: 'Busy Workflow' }]) + .mockResolvedValueOnce([{ id: 'log-1' }]) + + const result = await pollNoActivityEvents() + + expect(result.fired).toBe(0) + expect(result.skipped).toBe(1) + expect(mockDispatchSimEvent).not.toHaveBeenCalled() + }) + + it('only fires for the inactive workflow when watching several', async () => { + dbChainMockFns.limit + .mockResolvedValueOnce([makeSubscriptionRow(makeConfig())]) + .mockResolvedValueOnce([ + { id: 'wf-busy', name: 'Busy Workflow' }, + { id: 'wf-quiet', name: 'Quiet Workflow' }, + ]) + .mockResolvedValueOnce([{ id: 'log-1' }]) + .mockResolvedValueOnce([]) + + const result = await pollNoActivityEvents() + + expect(result.fired).toBe(1) + expect(mockDispatchSimEvent).toHaveBeenCalledTimes(1) + expect(mockDispatchSimEvent.mock.calls[0][1]).toMatchObject({ workflowId: 'wf-quiet' }) + }) + + it('cooldown is scoped per watched workflow: a cooled-down workflow does not suppress others', async () => { + mockReadLastFiredAt.mockImplementation((_wf: string, _block: string, scopeKey: string) => + Promise.resolve(scopeKey === 'wf-cooled' ? new Date() : null) + ) + dbChainMockFns.limit + .mockResolvedValueOnce([makeSubscriptionRow(makeConfig())]) + .mockResolvedValueOnce([ + { id: 'wf-cooled', name: 'Cooled Workflow' }, + { id: 'wf-quiet', name: 'Quiet Workflow' }, + ]) + .mockResolvedValueOnce([]) + + const result = await pollNoActivityEvents() + + expect(result.fired).toBe(1) + expect(result.skipped).toBe(1) + expect(mockDispatchSimEvent.mock.calls[0][1]).toMatchObject({ workflowId: 'wf-quiet' }) + }) + + it('a never-executed workflow fires once, then the lost claim suppresses repeats', async () => { + mockClaimCooldown.mockResolvedValueOnce(true).mockResolvedValueOnce(false) + + for (let poll = 0; poll < 2; poll++) { + dbChainMockFns.limit + .mockResolvedValueOnce([makeSubscriptionRow(makeConfig())]) + .mockResolvedValueOnce([{ id: 'wf-never-ran', name: 'Never Ran' }]) + .mockResolvedValueOnce([]) + } + + const first = await pollNoActivityEvents() + const second = await pollNoActivityEvents() + + expect(first.fired).toBe(1) + expect(second.fired).toBe(0) + expect(mockDispatchSimEvent).toHaveBeenCalledTimes(1) + }) + + it('scopes the watched-workflow query to the explicit selection in SQL (before the LIMIT)', async () => { + dbChainMockFns.limit + .mockResolvedValueOnce([makeSubscriptionRow(makeConfig({ workflowIds: ['wf-watched'] }))]) + .mockResolvedValueOnce([{ id: 'wf-watched', name: 'Watched' }]) + .mockResolvedValueOnce([]) + + const result = await pollNoActivityEvents() + + expect(result.checked).toBe(1) + expect(mockDispatchSimEvent).toHaveBeenCalledTimes(1) + expect(mockDispatchSimEvent.mock.calls[0][1]).toMatchObject({ workflowId: 'wf-watched' }) + expect(allWhereConditions()).toContainEqual( + expect.objectContaining({ type: 'inArray', values: ['wf-watched'] }) + ) + }) + + it('pages through subscriptions past the page size with a keyset cursor (no starvation)', async () => { + // Full first page of non-matching subscriptions (skipped without further + // queries), then a second page holding the one real no_activity + // subscription that must still be reached. + const firstPage = Array.from({ length: NO_ACTIVITY_SUBSCRIPTION_PAGE_SIZE }, (_, i) => + makeSubscriptionRow(makeConfig({ eventType: 'execution_error' }), `wh-page1-${i}`) + ) + dbChainMockFns.limit + .mockResolvedValueOnce(firstPage) + .mockResolvedValueOnce([makeSubscriptionRow(makeConfig(), 'wh-page2-0')]) + .mockResolvedValueOnce([{ id: 'wf-quiet', name: 'Quiet Workflow' }]) + .mockResolvedValueOnce([]) + + const result = await pollNoActivityEvents() + + expect(result.subscriptions).toBe(NO_ACTIVITY_SUBSCRIPTION_PAGE_SIZE + 1) + expect(result.fired).toBe(1) + expect(mockDispatchSimEvent.mock.calls[0][1]).toMatchObject({ workflowId: 'wf-quiet' }) + expect(allWhereConditions()).toContainEqual( + expect.objectContaining({ + type: 'gt', + right: `wh-page1-${NO_ACTIVITY_SUBSCRIPTION_PAGE_SIZE - 1}`, + }) + ) + }) + + it('pages through watched workflows past the page size with a keyset cursor (no lost coverage)', async () => { + // Full first page of watched workflows all inside their cooldown (skipped + // without activity queries), then a second page holding the quiet + // workflow that must still be reached. + mockReadLastFiredAt.mockImplementation((_wf: string, _block: string, scopeKey: string) => + Promise.resolve(scopeKey.startsWith('wf-p1-') ? new Date() : null) + ) + const firstPage = Array.from({ length: NO_ACTIVITY_WORKFLOW_PAGE_SIZE }, (_, i) => ({ + id: `wf-p1-${i}`, + name: `Workflow ${i}`, + })) + dbChainMockFns.limit + .mockResolvedValueOnce([makeSubscriptionRow(makeConfig())]) + .mockResolvedValueOnce(firstPage) + .mockResolvedValueOnce([{ id: 'wf-quiet', name: 'Quiet Workflow' }]) + .mockResolvedValueOnce([]) + + const result = await pollNoActivityEvents() + + expect(result.checked).toBe(NO_ACTIVITY_WORKFLOW_PAGE_SIZE + 1) + expect(result.skipped).toBe(NO_ACTIVITY_WORKFLOW_PAGE_SIZE) + expect(result.fired).toBe(1) + expect(mockDispatchSimEvent.mock.calls[0][1]).toMatchObject({ workflowId: 'wf-quiet' }) + expect(allWhereConditions()).toContainEqual( + expect.objectContaining({ + type: 'gt', + right: `wf-p1-${NO_ACTIVITY_WORKFLOW_PAGE_SIZE - 1}`, + }) + ) + }) + + it('excludes the subscriber workflow in SQL (before the LIMIT)', async () => { + dbChainMockFns.limit + .mockResolvedValueOnce([makeSubscriptionRow(makeConfig())]) + .mockResolvedValueOnce([]) + + const result = await pollNoActivityEvents() + + expect(result.checked).toBe(0) + expect(mockDispatchSimEvent).not.toHaveBeenCalled() + expect(allWhereConditions()).toContainEqual( + expect.objectContaining({ type: 'ne', right: 'wf-subscriber' }) + ) + }) +}) diff --git a/apps/sim/lib/workspace-events/no-activity.ts b/apps/sim/lib/workspace-events/no-activity.ts new file mode 100644 index 00000000000..de69868997a --- /dev/null +++ b/apps/sim/lib/workspace-events/no-activity.ts @@ -0,0 +1,274 @@ +import { db } from '@sim/db' +import { webhook, workflow, workflowDeploymentVersion, workflowExecutionLogs } from '@sim/db/schema' +import { createLogger } from '@sim/logger' +import { and, asc, eq, gt, gte, inArray, isNull, ne, or, sql } from 'drizzle-orm' +import { SIM_RULE_COOLDOWN_HOURS, SIM_TRIGGER_PROVIDER } from '@/lib/workspace-events/constants' +import { dispatchSimEvent } from '@/lib/workspace-events/emitter' +import { buildNoActivityEventPayload } from '@/lib/workspace-events/payload' +import { excludeSimExecutionsCondition } from '@/lib/workspace-events/rules' +import { claimCooldown, isWithinCooldown, readLastFiredAt } from '@/lib/workspace-events/state' +import { parseSubscriptionConfig } from '@/lib/workspace-events/subscriptions' +import type { SimSubscription, SimSubscriptionConfig } from '@/lib/workspace-events/types' + +const logger = createLogger('WorkspaceEventNoActivity') + +/** + * Page size for the keyset-paginated subscription scan. Every subscription is + * visited each poll — pagination bounds memory, not total work. + */ +export const NO_ACTIVITY_SUBSCRIPTION_PAGE_SIZE = 500 + +/** + * Page size for the keyset-paginated watched-workflow scan. Every watched + * workflow is visited each poll — pagination bounds memory, not total work. + */ +export const NO_ACTIVITY_WORKFLOW_PAGE_SIZE = 500 + +export interface NoActivityPollResult { + subscriptions: number + checked: number + fired: number + skipped: number +} + +/** + * Fetches one page of deployed Sim-trigger subscriptions configured for + * no_activity, across all workspaces, keyset-paginated by webhook id. A + * fixed cap would silently starve subscriptions beyond it; paging visits + * every subscription while keeping memory bounded. This runs from a + * low-frequency cron, so a global paged scan is acceptable — unlike the hot + * execution-completion path. + */ +async function fetchNoActivitySubscriptionPage( + afterWebhookId: string | null +): Promise { + const rows = await db + .select({ webhook, workflow }) + .from(webhook) + .innerJoin(workflow, eq(webhook.workflowId, workflow.id)) + .leftJoin( + workflowDeploymentVersion, + and( + eq(workflowDeploymentVersion.workflowId, workflow.id), + eq(workflowDeploymentVersion.isActive, true) + ) + ) + .where( + and( + eq(webhook.provider, SIM_TRIGGER_PROVIDER), + eq(webhook.isActive, true), + isNull(webhook.archivedAt), + eq(workflow.isDeployed, true), + isNull(workflow.archivedAt), + sql`${webhook.providerConfig}->>'eventType' = 'no_activity'`, + or( + eq(webhook.deploymentVersionId, workflowDeploymentVersion.id), + and(isNull(workflowDeploymentVersion.id), isNull(webhook.deploymentVersionId)) + ), + afterWebhookId === null ? undefined : gt(webhook.id, afterWebhookId) + ) + ) + .orderBy(asc(webhook.id)) + .limit(NO_ACTIVITY_SUBSCRIPTION_PAGE_SIZE) + + return rows +} + +/** + * Fetches one page of the workflows a no_activity subscription watches: + * deployed, active workflows in the subscriber's workspace, minus the + * subscriber itself, narrowed to the explicit selection when one is set + * (empty selection watches everything). Deployed-only keeps never-runnable + * draft workflows from alerting forever. Keyset-paginated by workflow id so + * watch-everything subscriptions in large workspaces never silently lose + * coverage past a cap. + */ +async function fetchWatchedWorkflowPage( + workspaceId: string, + subscriberWorkflowId: string, + config: SimSubscriptionConfig, + afterWorkflowId: string | null +): Promise> { + // Subscriber exclusion and the explicit selection must be SQL conditions: + // filtering in memory after the LIMIT could drop an explicitly watched + // workflow. The ORDER BY drives the keyset cursor. + const conditions = [ + eq(workflow.workspaceId, workspaceId), + eq(workflow.isDeployed, true), + isNull(workflow.archivedAt), + ne(workflow.id, subscriberWorkflowId), + ] + if (config.workflowIds.length > 0) { + conditions.push(inArray(workflow.id, config.workflowIds)) + } + if (afterWorkflowId !== null) { + conditions.push(gt(workflow.id, afterWorkflowId)) + } + + return db + .select({ id: workflow.id, name: workflow.name }) + .from(workflow) + .where(and(...conditions)) + .orderBy(asc(workflow.id)) + .limit(NO_ACTIVITY_WORKFLOW_PAGE_SIZE) +} + +/** True when the workflow had at least one qualifying execution inside the window. */ +async function hasRecentActivity( + workflowId: string, + config: SimSubscriptionConfig +): Promise { + const windowStart = new Date(Date.now() - config.inactivityHours * 60 * 60 * 1000) + + const recentLogs = await db + .select({ id: workflowExecutionLogs.id }) + .from(workflowExecutionLogs) + .where( + and( + eq(workflowExecutionLogs.workflowId, workflowId), + gte(workflowExecutionLogs.startedAt, windowStart), + excludeSimExecutionsCondition() + ) + ) + .limit(1) + + return recentLogs.length > 0 +} + +/** + * Cooldown for no_activity firings. At least the inactivity window itself — + * an hour-long cooldown with a multi-hour window would re-alert every hour + * for the same ongoing inactivity. + */ +function noActivityCooldownMs(config: SimSubscriptionConfig): number { + return Math.max(SIM_RULE_COOLDOWN_HOURS, config.inactivityHours) * 60 * 60 * 1000 +} + +/** + * Checks one watched workflow and fires when it has gone quiet, accumulating + * counts into `result`. + */ +async function checkWatchedWorkflow( + subscription: SimSubscription, + config: SimSubscriptionConfig, + sourceWorkflow: { id: string; name: string }, + result: NoActivityPollResult +): Promise { + result.checked++ + + const blockKey = subscription.webhook.blockId ?? subscription.webhook.path + const cooldownMs = noActivityCooldownMs(config) + + const lastFiredAt = await readLastFiredAt( + subscription.webhook.workflowId, + blockKey, + sourceWorkflow.id + ) + if (isWithinCooldown(lastFiredAt, cooldownMs)) { + result.skipped++ + return + } + + if (await hasRecentActivity(sourceWorkflow.id, config)) { + result.skipped++ + return + } + + const claimed = await claimCooldown( + subscription.webhook.workflowId, + blockKey, + sourceWorkflow.id, + cooldownMs + ) + if (!claimed) { + result.skipped++ + return + } + + const payload = buildNoActivityEventPayload({ + workflowId: sourceWorkflow.id, + workflowName: sourceWorkflow.name, + }) + + await dispatchSimEvent(subscription, payload) + result.fired++ + + logger.info(`no_activity event fired for workflow ${sourceWorkflow.id}`, { + subscriberWorkflowId: subscription.webhook.workflowId, + inactivityHours: config.inactivityHours, + }) +} + +/** + * Checks a single no_activity subscription's watched workflows and fires + * events for the inactive ones, accumulating counts into `result`. The + * watched-workflow scan is keyset-paginated, so coverage is complete even in + * workspaces with more workflows than one page. + */ +async function checkSubscription( + subscription: SimSubscription, + result: NoActivityPollResult +): Promise { + const config = parseSubscriptionConfig(subscription.webhook.providerConfig) + if (!config || config.eventType !== 'no_activity') return + + const workspaceId = subscription.workflow.workspaceId + if (!workspaceId) return + + let cursor: string | null = null + while (true) { + const page = await fetchWatchedWorkflowPage( + workspaceId, + subscription.webhook.workflowId, + config, + cursor + ) + if (page.length === 0) break + + cursor = page[page.length - 1].id + + for (const sourceWorkflow of page) { + await checkWatchedWorkflow(subscription, config, sourceWorkflow, result) + } + + if (page.length < NO_ACTIVITY_WORKFLOW_PAGE_SIZE) break + } +} + +/** + * Checks every no_activity subscription and fires side-effect workflows for + * watched workflows with no qualifying executions inside the window. The + * subscription scan is keyset-paginated, so every subscription is visited + * each poll regardless of how many exist. + * + * Cooldown state is keyed per (subscriber block × watched workflow), so one + * inactive workflow never suppresses alerts for others — a deliberate fix + * over the legacy per-subscription cooldown. A deployed workflow that has + * never executed fires once, then respects the cooldown. + */ +export async function pollNoActivityEvents(): Promise { + const result: NoActivityPollResult = { subscriptions: 0, checked: 0, fired: 0, skipped: 0 } + + let cursor: string | null = null + while (true) { + const page = await fetchNoActivitySubscriptionPage(cursor) + if (page.length === 0) break + + result.subscriptions += page.length + cursor = page[page.length - 1].webhook.id + + for (const subscription of page) { + await checkSubscription(subscription, result) + } + + if (page.length < NO_ACTIVITY_SUBSCRIPTION_PAGE_SIZE) break + } + + if (result.subscriptions === 0) return result + + logger.info( + `no_activity poll completed: ${result.fired} fired, ${result.skipped} skipped of ${result.checked} checked` + ) + + return result +} diff --git a/apps/sim/lib/workspace-events/payload.test.ts b/apps/sim/lib/workspace-events/payload.test.ts new file mode 100644 index 00000000000..f74b08a3b9d --- /dev/null +++ b/apps/sim/lib/workspace-events/payload.test.ts @@ -0,0 +1,144 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it } from 'vitest' +import { + SIM_EVENT_PAYLOAD_FIELDS, + SIM_FINAL_OUTPUT_MAX_BYTES, +} from '@/lib/workspace-events/constants' +import { + buildDeployEventPayload, + buildExecutionEventPayload, + buildNoActivityEventPayload, +} from '@/lib/workspace-events/payload' +import type { ExecutionEventContext } from '@/lib/workspace-events/types' + +const payloadKeys = Object.keys(SIM_EVENT_PAYLOAD_FIELDS).sort() + +function makeContext(overrides: Partial = {}): ExecutionEventContext { + return { + workflowId: 'wf-source', + executionId: 'exec-1', + status: 'error', + durationMs: 1000, + cost: 0.25, + finalOutput: { result: 42 }, + ...overrides, + } +} + +describe('payload builders align with the shared field constants', () => { + it('run-backed event payload has exactly the declared keys', () => { + const payload = buildExecutionEventPayload({ + event: 'execution_error', + workflowName: 'Source', + context: makeContext(), + }) + expect(Object.keys(payload).sort()).toEqual(payloadKeys) + expect(payload).toMatchObject({ + event: 'execution_error', + workflowId: 'wf-source', + workflowName: 'Source', + runId: 'exec-1', + durationMs: 1000, + // $0.25 reported as credits (1 credit = $0.005) + cost: 50, + }) + }) + + it('deploy event payload has exactly the declared keys with run fields null', () => { + const payload = buildDeployEventPayload({ + workflowId: 'wf-source', + workflowName: 'Source', + version: 3, + }) + expect(Object.keys(payload).sort()).toEqual(payloadKeys) + expect(payload).toMatchObject({ + event: 'workflow_deployed', + workflowId: 'wf-source', + workflowName: 'Source', + runId: null, + durationMs: null, + cost: null, + finalOutput: null, + version: 3, + }) + }) + + it('rule event payload nests the triggering run instead of top-level run fields', () => { + const payload = buildExecutionEventPayload({ + event: 'cost_threshold', + workflowName: 'Source', + context: makeContext(), + }) + expect(Object.keys(payload).sort()).toEqual(payloadKeys) + expect(payload).toMatchObject({ + event: 'cost_threshold', + runId: null, + durationMs: null, + cost: null, + finalOutput: null, + triggeringRun: { + runId: 'exec-1', + durationMs: 1000, + // $0.25 reported as credits (1 credit = $0.005) + cost: 50, + finalOutput: { result: 42 }, + }, + }) + }) + + it('no-activity payload has exactly the declared keys with run fields null', () => { + const payload = buildNoActivityEventPayload({ + workflowId: 'wf-source', + workflowName: 'Source', + }) + expect(Object.keys(payload).sort()).toEqual(payloadKeys) + expect(payload).toMatchObject({ + event: 'no_activity', + runId: null, + finalOutput: null, + }) + }) +}) + +describe('finalOutput handling', () => { + it('passes small outputs through untouched', () => { + const payload = buildExecutionEventPayload({ + event: 'execution_error', + workflowName: 'Source', + context: makeContext(), + }) + expect(payload.finalOutput).toEqual({ result: 42 }) + }) + + it('serializes and truncates oversized outputs', () => { + const huge = { blob: 'x'.repeat(SIM_FINAL_OUTPUT_MAX_BYTES + 1024) } + const payload = buildExecutionEventPayload({ + event: 'execution_error', + workflowName: 'Source', + context: makeContext({ finalOutput: huge }), + }) + expect(typeof payload.finalOutput).toBe('string') + expect((payload.finalOutput as string).length).toBeLessThanOrEqual(SIM_FINAL_OUTPUT_MAX_BYTES) + }) + + it('is nested under triggeringRun for rule events', () => { + const payload = buildExecutionEventPayload({ + event: 'cost_threshold', + workflowName: 'Source', + context: makeContext(), + }) + expect(payload.finalOutput).toBeNull() + expect(payload.triggeringRun?.finalOutput).toEqual({ result: 42 }) + }) + + it('is null when the source run produced no output', () => { + const payload = buildExecutionEventPayload({ + event: 'execution_success', + workflowName: 'Source', + context: makeContext({ status: 'success', finalOutput: undefined }), + }) + expect(payload.finalOutput).toBeNull() + }) +}) diff --git a/apps/sim/lib/workspace-events/payload.ts b/apps/sim/lib/workspace-events/payload.ts new file mode 100644 index 00000000000..8d1082ec13f --- /dev/null +++ b/apps/sim/lib/workspace-events/payload.ts @@ -0,0 +1,112 @@ +import { truncate } from '@sim/utils/string' +import { dollarsToCredits } from '@/lib/billing/credits/conversion' +import { + SIM_FINAL_OUTPUT_MAX_BYTES, + type SimEventType, + type SimPlainEventType, + type SimRuleEventType, +} from '@/lib/workspace-events/constants' +import type { + ExecutionEventContext, + SimEventPayload, + SimRunSummary, +} from '@/lib/workspace-events/types' + +/** + * Bounds the finalOutput field. Trace spans are never included; the payload + * travels through the job queue, so large outputs are serialized and + * truncated instead of being passed through whole. + */ +function boundFinalOutput(finalOutput: unknown): unknown { + if (finalOutput === undefined || finalOutput === null) return null + + try { + const serialized = JSON.stringify(finalOutput) + if (serialized === undefined) return null + if (serialized.length <= SIM_FINAL_OUTPUT_MAX_BYTES) return finalOutput + const suffix = '... [truncated]' + return truncate(serialized, SIM_FINAL_OUTPUT_MAX_BYTES - suffix.length, suffix) + } catch { + return null + } +} + +function basePayload(params: { + event: SimEventType + workflowId: string + workflowName: string +}): SimEventPayload { + return { + event: params.event, + timestamp: new Date().toISOString(), + workflowId: params.workflowId, + workflowName: params.workflowName, + runId: null, + durationMs: null, + cost: null, + finalOutput: null, + triggeringRun: null, + version: null, + } +} + +/** Run summary in user-facing units: cost in credits, finalOutput bounded. */ +function summarizeRun(context: ExecutionEventContext): SimRunSummary { + return { + runId: context.executionId, + durationMs: context.durationMs, + // Costs are stored in dollars; credits are the user-facing unit. + cost: dollarsToCredits(context.cost), + finalOutput: boundFinalOutput(context.finalOutput), + } +} + +/** + * Payload for run-backed events. Plain success/error events ARE the run, so + * its summary sits at the top level; for rule events the run is evidence for + * the condition that fired, so it nests under `triggeringRun`. + */ +export function buildExecutionEventPayload(params: { + event: Exclude | SimRuleEventType + workflowName: string + context: ExecutionEventContext +}): SimEventPayload { + const { event, workflowName, context } = params + + const base = basePayload({ event, workflowId: context.workflowId, workflowName }) + const run = summarizeRun(context) + + if (event === 'execution_success' || event === 'execution_error') { + return { ...base, ...run } + } + + return { ...base, triggeringRun: run } +} + +/** Payload for workflow_deployed events. */ +export function buildDeployEventPayload(params: { + workflowId: string + workflowName: string + version: number | null +}): SimEventPayload { + return { + ...basePayload({ + event: 'workflow_deployed', + workflowId: params.workflowId, + workflowName: params.workflowName, + }), + version: params.version, + } +} + +/** Payload for no_activity events (no source run exists). */ +export function buildNoActivityEventPayload(params: { + workflowId: string + workflowName: string +}): SimEventPayload { + return basePayload({ + event: 'no_activity', + workflowId: params.workflowId, + workflowName: params.workflowName, + }) +} diff --git a/apps/sim/lib/workspace-events/rules.test.ts b/apps/sim/lib/workspace-events/rules.test.ts new file mode 100644 index 00000000000..5350c25c079 --- /dev/null +++ b/apps/sim/lib/workspace-events/rules.test.ts @@ -0,0 +1,209 @@ +/** + * @vitest-environment node + */ +import { dbChainMock, dbChainMockFns } from '@sim/testing' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +vi.mock('@sim/db', () => dbChainMock) + +import { evaluateRule, excludeSimExecutionsCondition } from '@/lib/workspace-events/rules' +import type { ExecutionEventContext, SimSubscriptionConfig } from '@/lib/workspace-events/types' + +function makeConfig(overrides: Partial = {}): SimSubscriptionConfig { + return { + eventType: 'execution_error', + workflowIds: [], + consecutiveFailures: 3, + failureRatePercent: 50, + windowHours: 24, + durationThresholdMs: 30000, + latencySpikePercent: 100, + costThresholdCredits: 200, + errorCountThreshold: 10, + inactivityHours: 24, + ...overrides, + } +} + +function makeContext(overrides: Partial = {}): ExecutionEventContext { + return { + workflowId: 'wf-source', + executionId: 'exec-1', + status: 'error', + trigger: 'manual', + durationMs: 1000, + cost: 0.25, + errorMessage: 'boom', + finalOutput: null, + ...overrides, + } +} + +describe('excludeSimExecutionsCondition', () => { + it('excludes sim-triggered executions from rule statistics', () => { + const condition = excludeSimExecutionsCondition() as unknown as { + type: string + right?: unknown + } + expect(condition).toMatchObject({ type: 'ne', right: 'sim' }) + }) +}) + +describe('evaluateRule', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + describe('consecutive_failures', () => { + it('fires when the last N executions all failed', async () => { + dbChainMockFns.limit.mockResolvedValueOnce([ + { level: 'error' }, + { level: 'error' }, + { level: 'error' }, + ]) + await expect(evaluateRule('consecutive_failures', makeConfig(), makeContext())).resolves.toBe( + true + ) + }) + + it('does not fire when any recent execution succeeded', async () => { + dbChainMockFns.limit.mockResolvedValueOnce([ + { level: 'error' }, + { level: 'info' }, + { level: 'error' }, + ]) + await expect(evaluateRule('consecutive_failures', makeConfig(), makeContext())).resolves.toBe( + false + ) + }) + + it('does not fire with fewer executions than the threshold', async () => { + dbChainMockFns.limit.mockResolvedValueOnce([{ level: 'error' }, { level: 'error' }]) + await expect(evaluateRule('consecutive_failures', makeConfig(), makeContext())).resolves.toBe( + false + ) + }) + + it('only runs on failed executions', async () => { + await expect( + evaluateRule('consecutive_failures', makeConfig(), makeContext({ status: 'success' })) + ).resolves.toBe(false) + expect(dbChainMockFns.select).not.toHaveBeenCalled() + }) + }) + + describe('failure_rate', () => { + it('fires when the in-window failure rate meets the threshold (fixed legacy dead code)', async () => { + dbChainMockFns.where.mockImplementationOnce(() => Promise.resolve([{ total: 6, errors: 4 }])) + await expect(evaluateRule('failure_rate', makeConfig(), makeContext())).resolves.toBe(true) + }) + + it('does not fire below the minimum execution count', async () => { + dbChainMockFns.where.mockImplementationOnce(() => Promise.resolve([{ total: 4, errors: 4 }])) + await expect(evaluateRule('failure_rate', makeConfig(), makeContext())).resolves.toBe(false) + }) + + it('does not fire when the rate is below the threshold', async () => { + dbChainMockFns.where.mockImplementationOnce(() => Promise.resolve([{ total: 5, errors: 1 }])) + await expect(evaluateRule('failure_rate', makeConfig(), makeContext())).resolves.toBe(false) + }) + }) + + describe('latency_threshold', () => { + it('fires when duration exceeds the threshold', async () => { + await expect( + evaluateRule( + 'latency_threshold', + makeConfig({ durationThresholdMs: 1000 }), + makeContext({ durationMs: 1001 }) + ) + ).resolves.toBe(true) + }) + + it('does not fire at exactly the threshold', async () => { + await expect( + evaluateRule( + 'latency_threshold', + makeConfig({ durationThresholdMs: 1000 }), + makeContext({ durationMs: 1000 }) + ) + ).resolves.toBe(false) + }) + }) + + describe('latency_spike', () => { + it('fires when the execution is slower than the spike threshold over the average', async () => { + dbChainMockFns.where.mockImplementationOnce(() => + Promise.resolve([{ avgDuration: '1000', count: 5 }]) + ) + await expect( + evaluateRule('latency_spike', makeConfig(), makeContext({ durationMs: 2001 })) + ).resolves.toBe(true) + }) + + it('does not fire at exactly the spike threshold', async () => { + dbChainMockFns.where.mockImplementationOnce(() => + Promise.resolve([{ avgDuration: '1000', count: 5 }]) + ) + await expect( + evaluateRule('latency_spike', makeConfig(), makeContext({ durationMs: 2000 })) + ).resolves.toBe(false) + }) + + it('does not fire below the minimum execution count', async () => { + dbChainMockFns.where.mockImplementationOnce(() => + Promise.resolve([{ avgDuration: '1000', count: 4 }]) + ) + await expect( + evaluateRule('latency_spike', makeConfig(), makeContext({ durationMs: 5000 })) + ).resolves.toBe(false) + }) + }) + + describe('cost_threshold', () => { + it('fires when the run cost exceeds the credit-denominated threshold', async () => { + // 200 credits = $1; a $1.50 run exceeds it. + await expect( + evaluateRule( + 'cost_threshold', + makeConfig({ costThresholdCredits: 200 }), + makeContext({ cost: 1.5 }) + ) + ).resolves.toBe(true) + }) + + it('does not fire at exactly the threshold', async () => { + await expect( + evaluateRule( + 'cost_threshold', + makeConfig({ costThresholdCredits: 200 }), + makeContext({ cost: 1 }) + ) + ).resolves.toBe(false) + }) + }) + + describe('error_count', () => { + it('fires when the in-window error count reaches the threshold', async () => { + dbChainMockFns.where.mockImplementationOnce(() => Promise.resolve([{ count: 10 }])) + await expect(evaluateRule('error_count', makeConfig(), makeContext())).resolves.toBe(true) + }) + + it('does not fire below the threshold', async () => { + dbChainMockFns.where.mockImplementationOnce(() => Promise.resolve([{ count: 9 }])) + await expect(evaluateRule('error_count', makeConfig(), makeContext())).resolves.toBe(false) + }) + + it('only runs on failed executions', async () => { + await expect( + evaluateRule('error_count', makeConfig(), makeContext({ status: 'success' })) + ).resolves.toBe(false) + expect(dbChainMockFns.select).not.toHaveBeenCalled() + }) + }) + + it('no_activity never fires at execution time (owned by the poller)', async () => { + await expect(evaluateRule('no_activity', makeConfig(), makeContext())).resolves.toBe(false) + expect(dbChainMockFns.select).not.toHaveBeenCalled() + }) +}) diff --git a/apps/sim/lib/workspace-events/rules.ts b/apps/sim/lib/workspace-events/rules.ts new file mode 100644 index 00000000000..674d446076b --- /dev/null +++ b/apps/sim/lib/workspace-events/rules.ts @@ -0,0 +1,179 @@ +import { db } from '@sim/db' +import { workflowExecutionLogs } from '@sim/db/schema' +import { createLogger } from '@sim/logger' +import { and, avg, count, desc, eq, gte, ne, type SQL, sql } from 'drizzle-orm' +import { creditsToDollars } from '@/lib/billing/credits/conversion' +import { + SIM_MIN_EXECUTIONS_FOR_RATE_RULES, + SIM_TRIGGER_PROVIDER, + type SimRuleEventType, +} from '@/lib/workspace-events/constants' +import type { ExecutionEventContext, SimSubscriptionConfig } from '@/lib/workspace-events/types' + +const logger = createLogger('WorkspaceEventRules') + +/** + * Excludes executions started by the Sim trigger from rule statistics, so + * side-effect runs never pollute failure/latency counts for workflows that + * are both source and subscriber. + */ +export function excludeSimExecutionsCondition(): SQL { + return ne(workflowExecutionLogs.trigger, SIM_TRIGGER_PROVIDER) +} + +async function checkConsecutiveFailures(workflowId: string, threshold: number): Promise { + const recentLogs = await db + .select({ level: workflowExecutionLogs.level }) + .from(workflowExecutionLogs) + .where(and(eq(workflowExecutionLogs.workflowId, workflowId), excludeSimExecutionsCondition())) + .orderBy(desc(workflowExecutionLogs.startedAt)) + .limit(threshold) + + if (recentLogs.length < threshold) return false + + return recentLogs.every((log) => log.level === 'error') +} + +/** + * Fires when the in-window failure rate meets the threshold with at least + * SIM_MIN_EXECUTIONS_FOR_RATE_RULES executions. + * + * Intentionally diverges from the legacy notification rule, which required + * the oldest in-window log to predate the window start — a condition that is + * false for every in-window log, making the legacy rule dead code. + */ +async function checkFailureRate( + workflowId: string, + ratePercent: number, + windowHours: number +): Promise { + const windowStart = new Date(Date.now() - windowHours * 60 * 60 * 1000) + + // Single DB-side aggregate: the window is user-configured and this runs on + // the execution-completion path, so never materialize the in-window rows. + const result = await db + .select({ + total: count(), + errors: count(sql`case when ${workflowExecutionLogs.level} = 'error' then 1 end`), + }) + .from(workflowExecutionLogs) + .where( + and( + eq(workflowExecutionLogs.workflowId, workflowId), + gte(workflowExecutionLogs.startedAt, windowStart), + excludeSimExecutionsCondition() + ) + ) + + const total = result[0]?.total ?? 0 + if (total < SIM_MIN_EXECUTIONS_FOR_RATE_RULES) return false + + const errorCount = result[0]?.errors ?? 0 + const failureRate = (errorCount / total) * 100 + + return failureRate >= ratePercent +} + +async function checkLatencySpike( + workflowId: string, + currentDurationMs: number, + spikePercent: number, + windowHours: number +): Promise { + const windowStart = new Date(Date.now() - windowHours * 60 * 60 * 1000) + + const result = await db + .select({ + avgDuration: avg(workflowExecutionLogs.totalDurationMs), + count: count(), + }) + .from(workflowExecutionLogs) + .where( + and( + eq(workflowExecutionLogs.workflowId, workflowId), + gte(workflowExecutionLogs.startedAt, windowStart), + excludeSimExecutionsCondition() + ) + ) + + const avgDuration = result[0]?.avgDuration + const execCount = result[0]?.count || 0 + + if (!avgDuration || execCount < SIM_MIN_EXECUTIONS_FOR_RATE_RULES) return false + + const avgMs = Number(avgDuration) + const threshold = avgMs * (1 + spikePercent / 100) + + return currentDurationMs > threshold +} + +async function checkErrorCount( + workflowId: string, + threshold: number, + windowHours: number +): Promise { + const windowStart = new Date(Date.now() - windowHours * 60 * 60 * 1000) + + const result = await db + .select({ count: count() }) + .from(workflowExecutionLogs) + .where( + and( + eq(workflowExecutionLogs.workflowId, workflowId), + eq(workflowExecutionLogs.level, 'error'), + gte(workflowExecutionLogs.startedAt, windowStart), + excludeSimExecutionsCondition() + ) + ) + + const errorCount = result[0]?.count || 0 + return errorCount >= threshold +} + +/** + * Evaluates a rule-based event type against a completed execution. + * `no_activity` always returns false here — it has no triggering execution + * and is owned by the inactivity poller. + */ +export async function evaluateRule( + eventType: SimRuleEventType, + config: SimSubscriptionConfig, + context: ExecutionEventContext +): Promise { + switch (eventType) { + case 'consecutive_failures': + if (context.status !== 'error') return false + return checkConsecutiveFailures(context.workflowId, config.consecutiveFailures) + + case 'failure_rate': + if (context.status !== 'error') return false + return checkFailureRate(context.workflowId, config.failureRatePercent, config.windowHours) + + case 'latency_threshold': + return context.durationMs > config.durationThresholdMs + + case 'latency_spike': + return checkLatencySpike( + context.workflowId, + context.durationMs, + config.latencySpikePercent, + config.windowHours + ) + + case 'cost_threshold': + // The threshold is credit-denominated (the UI unit); run costs are + // stored in dollars, so convert the threshold for the comparison. + return context.cost > creditsToDollars(config.costThresholdCredits) + + case 'error_count': + if (context.status !== 'error') return false + return checkErrorCount(context.workflowId, config.errorCountThreshold, config.windowHours) + + case 'no_activity': + return false + + default: + logger.warn(`Unknown sim trigger rule: ${eventType}`) + return false + } +} diff --git a/apps/sim/lib/workspace-events/state.ts b/apps/sim/lib/workspace-events/state.ts new file mode 100644 index 00000000000..39c3c9ca2a0 --- /dev/null +++ b/apps/sim/lib/workspace-events/state.ts @@ -0,0 +1,72 @@ +import { db } from '@sim/db' +import { simTriggerState } from '@sim/db/schema' +import { and, eq, sql } from 'drizzle-orm' + +/** + * Reads the last firing time for a subscription scope. Cheap pre-check used + * to skip rule SQL while a subscription is cooling down; the atomic claim in + * {@link claimCooldown} remains the source of truth. + */ +export async function readLastFiredAt( + workflowId: string, + blockId: string, + scopeKey: string +): Promise { + const rows = await db + .select({ lastFiredAt: simTriggerState.lastFiredAt }) + .from(simTriggerState) + .where( + and( + eq(simTriggerState.workflowId, workflowId), + eq(simTriggerState.blockId, blockId), + eq(simTriggerState.scopeKey, scopeKey) + ) + ) + .limit(1) + + return rows[0]?.lastFiredAt ?? null +} + +/** + * Atomically claims a cooldown slot for a subscription scope. + * + * Uses an upsert whose update only applies when the previous firing is + * outside the cooldown window, so concurrent qualifying events can never + * double-fire: exactly one caller gets a row back. + * + * State is keyed by (workflowId, blockId, scopeKey) rather than the webhook + * row so cooldowns survive redeploys (webhook rows are recreated per + * deployment version). + */ +export async function claimCooldown( + workflowId: string, + blockId: string, + scopeKey: string, + cooldownMs: number +): Promise { + const now = new Date() + const threshold = new Date(now.getTime() - cooldownMs) + + const rows = await db + .insert(simTriggerState) + .values({ + workflowId, + blockId, + scopeKey, + lastFiredAt: now, + updatedAt: now, + }) + .onConflictDoUpdate({ + target: [simTriggerState.workflowId, simTriggerState.blockId, simTriggerState.scopeKey], + set: { lastFiredAt: now, updatedAt: now }, + setWhere: sql`${simTriggerState.lastFiredAt} IS NULL OR ${simTriggerState.lastFiredAt} < ${threshold}`, + }) + .returning({ workflowId: simTriggerState.workflowId }) + + return rows.length > 0 +} + +export function isWithinCooldown(lastFiredAt: Date | null, cooldownMs: number): boolean { + if (!lastFiredAt) return false + return Date.now() - lastFiredAt.getTime() < cooldownMs +} diff --git a/apps/sim/lib/workspace-events/subscriptions.test.ts b/apps/sim/lib/workspace-events/subscriptions.test.ts new file mode 100644 index 00000000000..08ccfbf7d88 --- /dev/null +++ b/apps/sim/lib/workspace-events/subscriptions.test.ts @@ -0,0 +1,75 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it } from 'vitest' +import { SIM_RULE_DEFAULTS } from '@/lib/workspace-events/constants' +import { parseSubscriptionConfig } from '@/lib/workspace-events/subscriptions' + +describe('parseSubscriptionConfig', () => { + it('returns null for configs without a recognizable event type', () => { + expect(parseSubscriptionConfig(null)).toBeNull() + expect(parseSubscriptionConfig({})).toBeNull() + expect(parseSubscriptionConfig({ eventType: 'bogus' })).toBeNull() + expect(parseSubscriptionConfig('not-an-object')).toBeNull() + }) + + it('parses workflow ids from arrays and comma-separated strings', () => { + expect( + parseSubscriptionConfig({ eventType: 'execution_error', workflowIds: ['a', 'b', ''] }) + ?.workflowIds + ).toEqual(['a', 'b']) + expect( + parseSubscriptionConfig({ eventType: 'execution_error', workflowIds: 'a, b,' })?.workflowIds + ).toEqual(['a', 'b']) + }) + + it('treats a missing workflow selection as watching every workflow (empty list)', () => { + expect(parseSubscriptionConfig({ eventType: 'execution_error' })?.workflowIds).toEqual([]) + }) + + it('coerces numeric rule fields and falls back to defaults for invalid values', () => { + const config = parseSubscriptionConfig({ + eventType: 'consecutive_failures', + consecutiveFailures: '5', + windowHours: 'not-a-number', + costThresholdCredits: -2, + }) + expect(config?.consecutiveFailures).toBe(5) + expect(config?.windowHours).toBe(SIM_RULE_DEFAULTS.windowHours) + expect(config?.costThresholdCredits).toBe(SIM_RULE_DEFAULTS.costThresholdCredits) + }) + + it('clamps rule fields to the legacy bounds (hot-path queries must stay bounded)', () => { + const config = parseSubscriptionConfig({ + eventType: 'failure_rate', + windowHours: 1_000_000, + consecutiveFailures: 5000, + failureRatePercent: 250, + durationThresholdMs: 5, + latencySpikePercent: 1, + costThresholdCredits: 10_000_000, + errorCountThreshold: 99999, + inactivityHours: 0.01, + }) + expect(config?.windowHours).toBe(168) + expect(config?.consecutiveFailures).toBe(100) + expect(config?.failureRatePercent).toBe(100) + expect(config?.durationThresholdMs).toBe(1000) + expect(config?.latencySpikePercent).toBe(10) + expect(config?.costThresholdCredits).toBe(200_000) + expect(config?.errorCountThreshold).toBe(1000) + expect(config?.inactivityHours).toBe(1) + }) + + it('rounds fractional integer fields (counts feed SQL LIMIT) but keeps credits fractional', () => { + const config = parseSubscriptionConfig({ + eventType: 'consecutive_failures', + consecutiveFailures: '2.5', + windowHours: 12.4, + costThresholdCredits: 250.5, + }) + expect(config?.consecutiveFailures).toBe(3) + expect(config?.windowHours).toBe(12) + expect(config?.costThresholdCredits).toBe(250.5) + }) +}) diff --git a/apps/sim/lib/workspace-events/subscriptions.ts b/apps/sim/lib/workspace-events/subscriptions.ts new file mode 100644 index 00000000000..d44ef32bc19 --- /dev/null +++ b/apps/sim/lib/workspace-events/subscriptions.ts @@ -0,0 +1,158 @@ +import { db } from '@sim/db' +import { webhook, workflow, workflowDeploymentVersion } from '@sim/db/schema' +import { and, eq, isNull, or } from 'drizzle-orm' +import { + SIM_EVENT_TYPES, + SIM_RULE_DEFAULTS, + SIM_TRIGGER_PROVIDER, + type SimEventType, +} from '@/lib/workspace-events/constants' +import type { SimSubscription, SimSubscriptionConfig } from '@/lib/workspace-events/types' + +/** + * Fetches active Sim-trigger subscriptions for one workspace. + * + * Workspace-scoped on purpose: execution completion is the hottest event in + * the platform, so this must never degrade into a global provider scan. The + * deployment-version join enforces that subscribers are deployed and the + * webhook row belongs to the active deployment version. + */ +export async function fetchSimTriggerSubscriptions( + workspaceId: string +): Promise { + const rows = await db + .select({ webhook, workflow }) + .from(webhook) + .innerJoin(workflow, eq(webhook.workflowId, workflow.id)) + .leftJoin( + workflowDeploymentVersion, + and( + eq(workflowDeploymentVersion.workflowId, workflow.id), + eq(workflowDeploymentVersion.isActive, true) + ) + ) + .where( + and( + eq(webhook.provider, SIM_TRIGGER_PROVIDER), + eq(webhook.isActive, true), + isNull(webhook.archivedAt), + eq(workflow.workspaceId, workspaceId), + eq(workflow.isDeployed, true), + isNull(workflow.archivedAt), + or( + eq(webhook.deploymentVersionId, workflowDeploymentVersion.id), + and(isNull(workflowDeploymentVersion.id), isNull(webhook.deploymentVersionId)) + ) + ) + ) + + return rows +} + +function parseStringArray(value: unknown): string[] { + if (Array.isArray(value)) { + return value.filter((entry): entry is string => typeof entry === 'string' && entry.length > 0) + } + if (typeof value === 'string' && value.length > 0) { + return value + .split(',') + .map((entry) => entry.trim()) + .filter(Boolean) + } + return [] +} + +/** + * Per-field bounds ported from the legacy notifications contract. Rule SQL + * runs on the execution-completion hot path, so windows and counts must stay + * inside the designed envelope regardless of what the free-text subblocks + * contain. Integer fields are rounded — counts feed SQL LIMIT, which rejects + * fractional values. The credit bounds are the legacy dollar bounds + * ($0.01-$1000) at 200 credits per dollar; credits stay fractional like the + * legacy dollar threshold. + */ +const SIM_RULE_BOUNDS = { + consecutiveFailures: { min: 1, max: 100, integer: true }, + failureRatePercent: { min: 1, max: 100, integer: true }, + windowHours: { min: 1, max: 168, integer: true }, + durationThresholdMs: { min: 1000, max: 3_600_000, integer: true }, + latencySpikePercent: { min: 10, max: 1000, integer: true }, + costThresholdCredits: { min: 2, max: 200_000 }, + errorCountThreshold: { min: 1, max: 1000, integer: true }, + inactivityHours: { min: 1, max: 168, integer: true }, +} as const + +function parseBoundedNumber( + value: unknown, + fallback: number, + bounds: { min: number; max: number; integer?: boolean } +): number { + const parsed = typeof value === 'number' ? value : Number(value) + if (!Number.isFinite(parsed) || parsed <= 0) return fallback + const clamped = Math.min(Math.max(parsed, bounds.min), bounds.max) + return bounds.integer ? Math.round(clamped) : clamped +} + +/** + * Parses a webhook row's providerConfig into a typed subscription config. + * Returns null when the config has no recognizable event type. + */ +export function parseSubscriptionConfig(providerConfig: unknown): SimSubscriptionConfig | null { + const config = + providerConfig && typeof providerConfig === 'object' && !Array.isArray(providerConfig) + ? (providerConfig as Record) + : {} + + const eventType = config.eventType + if ( + typeof eventType !== 'string' || + !(SIM_EVENT_TYPES as readonly string[]).includes(eventType) + ) { + return null + } + + return { + eventType: eventType as SimEventType, + workflowIds: parseStringArray(config.workflowIds), + consecutiveFailures: parseBoundedNumber( + config.consecutiveFailures, + SIM_RULE_DEFAULTS.consecutiveFailures, + SIM_RULE_BOUNDS.consecutiveFailures + ), + failureRatePercent: parseBoundedNumber( + config.failureRatePercent, + SIM_RULE_DEFAULTS.failureRatePercent, + SIM_RULE_BOUNDS.failureRatePercent + ), + windowHours: parseBoundedNumber( + config.windowHours, + SIM_RULE_DEFAULTS.windowHours, + SIM_RULE_BOUNDS.windowHours + ), + durationThresholdMs: parseBoundedNumber( + config.durationThresholdMs, + SIM_RULE_DEFAULTS.durationThresholdMs, + SIM_RULE_BOUNDS.durationThresholdMs + ), + latencySpikePercent: parseBoundedNumber( + config.latencySpikePercent, + SIM_RULE_DEFAULTS.latencySpikePercent, + SIM_RULE_BOUNDS.latencySpikePercent + ), + costThresholdCredits: parseBoundedNumber( + config.costThresholdCredits, + SIM_RULE_DEFAULTS.costThresholdCredits, + SIM_RULE_BOUNDS.costThresholdCredits + ), + errorCountThreshold: parseBoundedNumber( + config.errorCountThreshold, + SIM_RULE_DEFAULTS.errorCountThreshold, + SIM_RULE_BOUNDS.errorCountThreshold + ), + inactivityHours: parseBoundedNumber( + config.inactivityHours, + SIM_RULE_DEFAULTS.inactivityHours, + SIM_RULE_BOUNDS.inactivityHours + ), + } +} diff --git a/apps/sim/lib/workspace-events/types.ts b/apps/sim/lib/workspace-events/types.ts new file mode 100644 index 00000000000..427894b772b --- /dev/null +++ b/apps/sim/lib/workspace-events/types.ts @@ -0,0 +1,64 @@ +import type { webhook, workflow } from '@sim/db/schema' +import type { SimEventPayloadFieldKey, SimEventType } from '@/lib/workspace-events/constants' + +/** A deployed Sim-trigger block subscribed to workspace events. */ +export interface SimSubscription { + webhook: typeof webhook.$inferSelect + workflow: typeof workflow.$inferSelect +} + +/** + * Parsed, coerced subscription configuration. Provider config values arrive as + * raw subblock values (numbers as strings, arrays sometimes serialized), so all + * consumers go through the parser in subscriptions.ts rather than reading + * providerConfig directly. + */ +export interface SimSubscriptionConfig { + eventType: SimEventType + /** Source workflows to watch. Empty means every workflow in the workspace. */ + workflowIds: string[] + consecutiveFailures: number + failureRatePercent: number + windowHours: number + durationThresholdMs: number + latencySpikePercent: number + costThresholdCredits: number + errorCountThreshold: number + inactivityHours: number +} + +/** Facts a completed run contributes to event matching and rule evaluation. */ +export interface ExecutionEventContext { + workflowId: string + executionId: string + status: 'success' | 'error' + durationMs: number + /** Run cost in dollars (the storage unit); converted to credits at the payload boundary. */ + cost: number + finalOutput: unknown +} + +/** Summary of the run behind an event, in user-facing units (cost in credits). */ +export interface SimRunSummary { + runId: string + durationMs: number + cost: number + finalOutput: unknown +} + +/** + * Wire payload delivered to a Sim trigger workflow. Keys must align with + * SIM_EVENT_PAYLOAD_FIELDS — enforced by tests on the payload builders. + */ +export type SimEventPayload = Record & { + event: SimEventType + timestamp: string + workflowId: string + workflowName: string + runId: string | null + durationMs: number | null + cost: number | null + finalOutput: unknown + triggeringRun: SimRunSummary | null + version: number | null +} diff --git a/apps/sim/lib/workspaces/lifecycle.test.ts b/apps/sim/lib/workspaces/lifecycle.test.ts index 070b9c4ff25..1013b2280f4 100644 --- a/apps/sim/lib/workspaces/lifecycle.test.ts +++ b/apps/sim/lib/workspaces/lifecycle.test.ts @@ -78,7 +78,7 @@ describe('workspace lifecycle', () => { expect(mockArchiveWorkflowsForWorkspace).toHaveBeenCalledWith('workspace-1', { requestId: 'req-1', }) - expect(tx.update).toHaveBeenCalledTimes(11) + expect(tx.update).toHaveBeenCalledTimes(10) expect(tx.delete).toHaveBeenCalledTimes(1) }) diff --git a/apps/sim/lib/workspaces/lifecycle.ts b/apps/sim/lib/workspaces/lifecycle.ts index b0a2b0d6161..59162b42682 100644 --- a/apps/sim/lib/workspaces/lifecycle.ts +++ b/apps/sim/lib/workspaces/lifecycle.ts @@ -12,7 +12,6 @@ import { workflowSchedule, workspace, workspaceFiles, - workspaceNotificationSubscription, } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { and, eq, inArray, isNull, sql } from 'drizzle-orm' @@ -107,14 +106,6 @@ export async function archiveWorkspace( }) .where(and(eq(workspaceFiles.workspaceId, workspaceId), isNull(workspaceFiles.deletedAt))) - await tx - .update(workspaceNotificationSubscription) - .set({ - active: false, - updatedAt: now, - }) - .where(eq(workspaceNotificationSubscription.workspaceId, workspaceId)) - await tx .update(invitation) .set({ diff --git a/apps/sim/tools/logs/get_run_details.ts b/apps/sim/tools/logs/get_run_details.ts new file mode 100644 index 00000000000..6326e3afcbd --- /dev/null +++ b/apps/sim/tools/logs/get_run_details.ts @@ -0,0 +1,75 @@ +import type { WorkflowLogDetail } from '@/lib/api/contracts/logs' +import { dollarsToCredits } from '@/lib/billing/credits/conversion' +import type { LogsGetRunDetailsParams, LogsGetRunDetailsResponse } from '@/tools/logs/types' +import type { ToolConfig } from '@/tools/types' + +export const logsGetRunDetailsTool: ToolConfig = + { + id: 'logs_get_run_details', + name: 'Get Run Details', + description: + 'Fetch details for a single workflow run by its run ID, including the full trace spans.', + version: '1.0.0', + + params: { + runId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The run ID to fetch details for', + }, + }, + + request: { + url: (params) => { + const workspaceId = params._context?.workspaceId + if (!workspaceId) { + throw new Error('workspaceId is required in execution context') + } + const qs = new URLSearchParams({ workspaceId }) + return `/api/logs/by-execution/${encodeURIComponent(params.runId)}?${qs.toString()}` + }, + method: 'GET', + headers: () => ({ + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response): Promise => { + const result = await response.json() + if (!response.ok) { + throw new Error(result?.error || `Request failed with status ${response.status}`) + } + const detail: WorkflowLogDetail = result.data + + return { + success: true, + output: { + runId: detail.executionId ?? '', + workflowId: detail.workflowId ?? null, + workflowName: detail.workflow?.name ?? null, + status: detail.status ?? detail.level, + trigger: detail.trigger ?? null, + startedAt: detail.createdAt, + durationMs: detail.executionData?.totalDuration ?? null, + // Costs are stored in dollars; credits are the user-facing unit. + cost: detail.cost?.total != null ? dollarsToCredits(detail.cost.total) : null, + traceSpans: detail.executionData?.traceSpans ?? [], + finalOutput: detail.executionData?.finalOutput ?? null, + }, + } + }, + + outputs: { + runId: { type: 'string', description: 'The run ID' }, + workflowId: { type: 'string', description: 'Workflow ID this run belongs to' }, + workflowName: { type: 'string', description: 'Workflow name' }, + status: { type: 'string', description: 'Run status' }, + trigger: { type: 'string', description: 'How the run was triggered' }, + startedAt: { type: 'string', description: 'Run start time (ISO 8601)' }, + durationMs: { type: 'number', description: 'Run duration in milliseconds' }, + cost: { type: 'number', description: 'Run cost in credits' }, + traceSpans: { type: 'array', description: 'Full trace spans for the run' }, + finalOutput: { type: 'json', description: 'Final output of the run' }, + }, + } diff --git a/apps/sim/tools/logs/index.ts b/apps/sim/tools/logs/index.ts index 109d223c8b8..680d9f9e17b 100644 --- a/apps/sim/tools/logs/index.ts +++ b/apps/sim/tools/logs/index.ts @@ -1,3 +1,5 @@ export { logsGetExecutionTool } from '@/tools/logs/get_execution' export { logsGetTool } from '@/tools/logs/get_log' +export { logsGetRunDetailsTool } from '@/tools/logs/get_run_details' export { logsQueryTool } from '@/tools/logs/query' +export { logsQueryRunsTool } from '@/tools/logs/query_runs' diff --git a/apps/sim/tools/logs/query_runs.ts b/apps/sim/tools/logs/query_runs.ts new file mode 100644 index 00000000000..6c134a317c1 --- /dev/null +++ b/apps/sim/tools/logs/query_runs.ts @@ -0,0 +1,163 @@ +import { creditsToDollars } from '@/lib/billing/credits/conversion' +import type { LogsQueryRunsParams, LogsQueryRunsResponse } from '@/tools/logs/types' +import type { ToolConfig } from '@/tools/types' + +export const logsQueryRunsTool: ToolConfig = { + id: 'logs_query_runs', + name: 'Query Logs', + description: + 'Query workflow run logs in the current workspace with the full Logs-page filter set. Returns matching run IDs.', + version: '1.0.0', + + params: { + workflowIds: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Comma-separated workflow IDs to filter by', + }, + folderIds: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Comma-separated folder IDs to filter by (descendants included)', + }, + level: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + "Comma-separated statuses: 'info', 'error', 'running', 'pending', 'cancelled'. Omit for all.", + }, + triggers: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Comma-separated trigger types (api, webhook, schedule, manual, chat, mcp, a2a, workflow, sim, …)', + }, + startDate: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'ISO 8601 timestamp; only runs at or after this time', + }, + endDate: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'ISO 8601 timestamp; only runs at or before this time', + }, + search: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Free-text search across log fields', + }, + costOperator: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: "Cost comparison operator: '=', '>', '<', '>=', '<=', '!='", + }, + costValue: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Cost threshold in credits, compared using costOperator', + }, + durationOperator: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: "Duration comparison operator: '=', '>', '<', '>=', '<=', '!='", + }, + durationValue: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Duration threshold in milliseconds, compared using durationOperator', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Max run IDs to return (default 100, max 200)', + }, + sortBy: { + type: 'string', + required: false, + visibility: 'user-only', + description: "Sort field: 'date' (default), 'duration', 'cost', 'status'", + }, + sortOrder: { + type: 'string', + required: false, + visibility: 'user-only', + description: "Sort order: 'desc' (default) or 'asc'", + }, + }, + + request: { + url: (params) => { + const workspaceId = params._context?.workspaceId + if (!workspaceId) { + throw new Error('workspaceId is required in execution context') + } + const qs = new URLSearchParams({ workspaceId }) + if (params.workflowIds) qs.set('workflowIds', params.workflowIds) + if (params.folderIds) qs.set('folderIds', params.folderIds) + if (params.level && params.level !== 'all') qs.set('level', params.level) + if (params.triggers) qs.set('triggers', params.triggers) + if (params.startDate) qs.set('startDate', params.startDate) + if (params.endDate) qs.set('endDate', params.endDate) + if (params.search) qs.set('search', params.search) + if (params.costOperator && params.costValue !== undefined && params.costValue !== null) { + qs.set('costOperator', params.costOperator) + // Costs are credit-denominated for users; the API filters in dollars. + qs.set('costValue', String(creditsToDollars(params.costValue))) + } + if ( + params.durationOperator && + params.durationValue !== undefined && + params.durationValue !== null + ) { + qs.set('durationOperator', params.durationOperator) + qs.set('durationValue', String(params.durationValue)) + } + if (params.limit !== undefined && params.limit !== null) { + qs.set('limit', String(params.limit)) + } + if (params.sortBy) qs.set('sortBy', params.sortBy) + if (params.sortOrder) qs.set('sortOrder', params.sortOrder) + return `/api/logs?${qs.toString()}` + }, + method: 'GET', + headers: () => ({ + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response): Promise => { + const result = await response.json() + if (!response.ok) { + throw new Error(result?.error || `Request failed with status ${response.status}`) + } + const rows: Array<{ executionId?: string | null }> = result.data || [] + return { + success: true, + output: { + runIds: rows + .map((row) => row.executionId) + .filter((runId): runId is string => Boolean(runId)), + }, + } + }, + + outputs: { + runIds: { + type: 'array', + description: 'IDs of the runs matching the filters', + }, + }, +} diff --git a/apps/sim/tools/logs/types.ts b/apps/sim/tools/logs/types.ts index 3053059b1f1..b8c6f8f074a 100644 --- a/apps/sim/tools/logs/types.ts +++ b/apps/sim/tools/logs/types.ts @@ -30,6 +30,31 @@ export interface LogsGetExecutionParams { _context?: WorkflowToolExecutionContext } +export type LogsComparisonOperator = '=' | '>' | '<' | '>=' | '<=' | '!=' + +export interface LogsQueryRunsParams { + workflowIds?: string + folderIds?: string + level?: string + triggers?: string + startDate?: string + endDate?: string + search?: string + costOperator?: LogsComparisonOperator + costValue?: number + durationOperator?: LogsComparisonOperator + durationValue?: number + limit?: number + sortBy?: 'date' | 'duration' | 'cost' | 'status' + sortOrder?: 'asc' | 'desc' + _context?: WorkflowToolExecutionContext +} + +export interface LogsGetRunDetailsParams { + runId: string + _context?: WorkflowToolExecutionContext +} + export interface LogsQueryResponse extends ToolResponse { output: { logs: WorkflowLogSummary[] @@ -37,6 +62,27 @@ export interface LogsQueryResponse extends ToolResponse { } } +export interface LogsQueryRunsResponse extends ToolResponse { + output: { + runIds: string[] + } +} + +export interface LogsGetRunDetailsResponse extends ToolResponse { + output: { + runId: string + workflowId: string | null + workflowName: string | null + status: string + trigger: string | null + startedAt: string + durationMs: number | null + cost: number | null + traceSpans: unknown[] + finalOutput: unknown + } +} + export interface LogsGetResponse extends ToolResponse { output: { log: WorkflowLogDetail diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 740202ee7fd..c5f425da89f 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -1818,7 +1818,13 @@ import { linqUpdateWebhookSubscriptionTool, } from '@/tools/linq' import { llmChatTool } from '@/tools/llm' -import { logsGetExecutionTool, logsGetTool, logsQueryTool } from '@/tools/logs' +import { + logsGetExecutionTool, + logsGetRunDetailsTool, + logsGetTool, + logsQueryRunsTool, + logsQueryTool, +} from '@/tools/logs' import { loopsCreateContactPropertyTool, loopsCreateContactTool, @@ -3758,8 +3764,10 @@ export const tools: Record = { linq_update_contact_card: linqUpdateContactCardTool, linq_update_webhook_subscription: linqUpdateWebhookSubscriptionTool, logs_query: logsQueryTool, + logs_query_runs: logsQueryRunsTool, logs_get: logsGetTool, logs_get_execution: logsGetExecutionTool, + logs_get_run_details: logsGetRunDetailsTool, loops_create_contact: loopsCreateContactTool, loops_create_contact_property: loopsCreateContactPropertyTool, loops_update_contact: loopsUpdateContactTool, diff --git a/apps/sim/triggers/constants.ts b/apps/sim/triggers/constants.ts index 57eab0b7751..bd1ca98de0d 100644 --- a/apps/sim/triggers/constants.ts +++ b/apps/sim/triggers/constants.ts @@ -56,3 +56,15 @@ export const POLLING_PROVIDERS = new Set([ export function isPollingWebhookProvider(provider: string): boolean { return POLLING_PROVIDERS.has(provider) } + +/** + * Providers whose triggers fire internally (table row events, Sim workspace + * events) rather than via external HTTP webhooks. Their webhook rows still + * register a path, so the public trigger route must reject deliveries to + * them — otherwise anyone with the block ID could forge events. + */ +export const INTERNAL_TRIGGER_PROVIDERS = new Set(['sim', 'table']) + +export function isInternalTriggerProvider(provider: string | null): boolean { + return provider !== null && INTERNAL_TRIGGER_PROVIDERS.has(provider) +} diff --git a/apps/sim/triggers/registry.ts b/apps/sim/triggers/registry.ts index d6c0c40b25f..d388c8ab303 100644 --- a/apps/sim/triggers/registry.ts +++ b/apps/sim/triggers/registry.ts @@ -301,6 +301,7 @@ import { servicenowIncidentUpdatedTrigger, servicenowWebhookTrigger, } from '@/triggers/servicenow' +import { simWorkspaceEventTrigger } from '@/triggers/sim' import { slackWebhookTrigger } from '@/triggers/slack' import { stripeWebhookTrigger } from '@/triggers/stripe' import { tableNewRowTrigger } from '@/triggers/table' @@ -564,6 +565,7 @@ export const TRIGGER_REGISTRY: TriggerRegistry = { servicenow_change_request_created: servicenowChangeRequestCreatedTrigger, servicenow_change_request_updated: servicenowChangeRequestUpdatedTrigger, servicenow_webhook: servicenowWebhookTrigger, + sim_workspace_event: simWorkspaceEventTrigger, stripe_webhook: stripeWebhookTrigger, table_new_row: tableNewRowTrigger, telegram_webhook: telegramWebhookTrigger, diff --git a/apps/sim/triggers/sim/index.ts b/apps/sim/triggers/sim/index.ts new file mode 100644 index 00000000000..964fe9475ed --- /dev/null +++ b/apps/sim/triggers/sim/index.ts @@ -0,0 +1 @@ +export { simWorkspaceEventTrigger } from '@/triggers/sim/workspace-event' diff --git a/apps/sim/triggers/sim/workspace-event.test.ts b/apps/sim/triggers/sim/workspace-event.test.ts new file mode 100644 index 00000000000..9d65c95b678 --- /dev/null +++ b/apps/sim/triggers/sim/workspace-event.test.ts @@ -0,0 +1,255 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it } from 'vitest' +import { + SIM_EVENT_PAYLOAD_FIELDS, + SIM_EVENT_TYPES, + SIM_RULE_DEFAULTS, + SIM_RULE_EVENT_TYPES, + SIM_TRIGGER_PROVIDER, + SIM_WORKSPACE_EVENT_TRIGGER_ID, +} from '@/lib/workspace-events/constants' +import { SimWorkspaceEventBlock } from '@/blocks/blocks/sim_workspace_event' +import { TRIGGER_REGISTRY } from '@/triggers/registry' +import { simWorkspaceEventTrigger } from '@/triggers/sim' + +describe('sim workspace event trigger registration', () => { + it('is registered in the trigger registry under its trigger ID', () => { + expect(TRIGGER_REGISTRY[SIM_WORKSPACE_EVENT_TRIGGER_ID]).toBe(simWorkspaceEventTrigger) + }) + + it('uses the sim provider and is purely internal (no webhook, no polling)', () => { + expect(simWorkspaceEventTrigger.provider).toBe(SIM_TRIGGER_PROVIDER) + expect(simWorkspaceEventTrigger.webhook).toBeUndefined() + expect(simWorkspaceEventTrigger.polling).toBeUndefined() + }) + + it('block type equals the trigger ID so deploy-time trigger resolution works', () => { + expect(SimWorkspaceEventBlock.type).toBe(SIM_WORKSPACE_EVENT_TRIGGER_ID) + expect(SimWorkspaceEventBlock.category).toBe('triggers') + expect(SimWorkspaceEventBlock.triggers).toEqual({ + enabled: true, + available: [SIM_WORKSPACE_EVENT_TRIGGER_ID], + }) + }) + + it('is named Sim', () => { + expect(SimWorkspaceEventBlock.name).toBe('Sim') + expect(simWorkspaceEventTrigger.name).toBe('Sim') + }) +}) + +describe('sim workspace event subblocks', () => { + it('all subblocks are trigger-mode with unique IDs', () => { + const ids = simWorkspaceEventTrigger.subBlocks.map((subBlock) => subBlock.id) + expect(new Set(ids).size).toBe(ids.length) + for (const subBlock of simWorkspaceEventTrigger.subBlocks) { + expect(subBlock.mode, `subblock ${subBlock.id} must be trigger-mode`).toBe('trigger') + } + }) + + it('the eventType dropdown covers every event type exactly once', () => { + const eventTypeSubBlock = simWorkspaceEventTrigger.subBlocks.find( + (subBlock) => subBlock.id === 'eventType' + ) + expect(eventTypeSubBlock).toBeDefined() + const optionIds = (eventTypeSubBlock!.options as Array<{ id: string; label: string }>).map( + (option) => option.id + ) + expect(optionIds.sort()).toEqual([...SIM_EVENT_TYPES].sort()) + }) + + it('the workflow multi-select is always visible and optional (empty = all workflows)', () => { + const workflowIds = simWorkspaceEventTrigger.subBlocks.find((sb) => sb.id === 'workflowIds') + expect(workflowIds).toBeDefined() + expect(workflowIds!.condition).toBeUndefined() + expect(workflowIds!.required).toBe(false) + expect(workflowIds!.multiSelect).toBe(true) + expect(workflowIds!.placeholder).toBe('All workflows') + }) + + it('has no source-trigger filter or finalOutput toggle', () => { + const ids = simWorkspaceEventTrigger.subBlocks.map((sb) => sb.id) + expect(ids).not.toContain('triggerFilter') + expect(ids).not.toContain('includeFinalOutput') + expect(ids).not.toContain('allWorkflows') + }) + + it('every rule event type has a config subblock gated to it with the ported default', () => { + const expectations: Array<{ id: string; eventType: string; defaultValue: string }> = [ + { + id: 'consecutiveFailures', + eventType: 'consecutive_failures', + defaultValue: String(SIM_RULE_DEFAULTS.consecutiveFailures), + }, + { + id: 'failureRatePercent', + eventType: 'failure_rate', + defaultValue: String(SIM_RULE_DEFAULTS.failureRatePercent), + }, + { + id: 'durationThresholdMs', + eventType: 'latency_threshold', + defaultValue: String(SIM_RULE_DEFAULTS.durationThresholdMs), + }, + { + id: 'latencySpikePercent', + eventType: 'latency_spike', + defaultValue: String(SIM_RULE_DEFAULTS.latencySpikePercent), + }, + { + id: 'costThresholdCredits', + eventType: 'cost_threshold', + defaultValue: String(SIM_RULE_DEFAULTS.costThresholdCredits), + }, + { + id: 'errorCountThreshold', + eventType: 'error_count', + defaultValue: String(SIM_RULE_DEFAULTS.errorCountThreshold), + }, + { + id: 'inactivityHours', + eventType: 'no_activity', + defaultValue: String(SIM_RULE_DEFAULTS.inactivityHours), + }, + ] + + for (const expectation of expectations) { + const subBlock = simWorkspaceEventTrigger.subBlocks.find((sb) => sb.id === expectation.id) + expect(subBlock, `missing config subblock ${expectation.id}`).toBeDefined() + expect(subBlock!.defaultValue).toBe(expectation.defaultValue) + const condition = subBlock!.condition as { field: string; value: string } + expect(condition.field).toBe('eventType') + expect(condition.value).toBe(expectation.eventType) + } + + const windowHours = simWorkspaceEventTrigger.subBlocks.find((sb) => sb.id === 'windowHours') + expect(windowHours).toBeDefined() + const windowCondition = windowHours!.condition as { field: string; value: string[] } + expect(windowCondition.field).toBe('eventType') + expect(windowCondition.value.sort()).toEqual( + ['error_count', 'failure_rate', 'latency_spike'].sort() + ) + }) + + it('rule config subblocks are gated only to rule event types', () => { + const ruleConfigIds = [ + 'consecutiveFailures', + 'failureRatePercent', + 'durationThresholdMs', + 'latencySpikePercent', + 'costThresholdCredits', + 'errorCountThreshold', + 'inactivityHours', + 'windowHours', + ] + for (const id of ruleConfigIds) { + const subBlock = simWorkspaceEventTrigger.subBlocks.find((sb) => sb.id === id) + const condition = subBlock!.condition as { field: string; value: string | string[] } + const gatedTo = Array.isArray(condition.value) ? condition.value : [condition.value] + for (const eventType of gatedTo) { + expect( + (SIM_RULE_EVENT_TYPES as readonly string[]).includes(eventType), + `${id} is gated to non-rule event type ${eventType}` + ).toBe(true) + } + } + }) +}) + +describe('sim workspace event outputs', () => { + const EXECUTION_BACKED = [ + 'execution_success', + 'execution_error', + 'consecutive_failures', + 'failure_rate', + 'latency_threshold', + 'latency_spike', + 'cost_threshold', + 'error_count', + ] + + /** Output keys expected in the tag dropdown for a given event type. */ + function visibleOutputsFor(eventType: string): string[] { + return Object.entries(simWorkspaceEventTrigger.outputs) + .filter(([, definition]) => { + const condition = (definition as { condition?: { field: string; value: unknown } }) + .condition + if (!condition) return true + const values = Array.isArray(condition.value) ? condition.value : [condition.value] + return values.includes(eventType) + }) + .map(([key]) => key) + .sort() + } + + it('trigger outputs align key-for-key with the shared payload field constants', () => { + const outputKeys = Object.keys(simWorkspaceEventTrigger.outputs).sort() + const payloadKeys = Object.keys(SIM_EVENT_PAYLOAD_FIELDS).sort() + expect(outputKeys).toEqual(payloadKeys) + }) + + it('output conditions only reference the eventType field with valid event types', () => { + for (const [key, definition] of Object.entries(simWorkspaceEventTrigger.outputs)) { + const condition = (definition as { condition?: { field: string; value: unknown } }).condition + if (!condition) continue + expect(condition.field, `${key} condition must gate on eventType`).toBe('eventType') + const values = Array.isArray(condition.value) ? condition.value : [condition.value] + for (const value of values) { + expect( + (SIM_EVENT_TYPES as readonly string[]).includes(value as string), + `${key} condition references unknown event type ${value}` + ).toBe(true) + } + } + }) + + it('plain run events expose the base fields plus the top-level run summary', () => { + for (const eventType of ['execution_success', 'execution_error']) { + expect(visibleOutputsFor(eventType)).toEqual( + [ + 'cost', + 'durationMs', + 'event', + 'finalOutput', + 'runId', + 'timestamp', + 'workflowId', + 'workflowName', + ].sort() + ) + } + }) + + it('run-backed rule events expose the base fields plus the nested triggeringRun', () => { + for (const eventType of EXECUTION_BACKED.filter( + (type) => type !== 'execution_success' && type !== 'execution_error' + )) { + expect(visibleOutputsFor(eventType)).toEqual( + ['event', 'timestamp', 'triggeringRun', 'workflowId', 'workflowName'].sort() + ) + } + }) + + it('triggeringRun nests the same run summary fields as plain run events', () => { + const triggeringRun = simWorkspaceEventTrigger.outputs.triggeringRun as { + properties?: Record + } + expect(Object.keys(triggeringRun.properties ?? {}).sort()).toEqual( + ['cost', 'durationMs', 'finalOutput', 'runId'].sort() + ) + }) + + it('workflow_deployed exposes the base fields plus the version', () => { + expect(visibleOutputsFor('workflow_deployed')).toEqual( + ['event', 'timestamp', 'version', 'workflowId', 'workflowName'].sort() + ) + }) + + it('no_activity exposes only the base fields', () => { + expect(visibleOutputsFor('no_activity')).toEqual( + ['event', 'timestamp', 'workflowId', 'workflowName'].sort() + ) + }) +}) diff --git a/apps/sim/triggers/sim/workspace-event.ts b/apps/sim/triggers/sim/workspace-event.ts new file mode 100644 index 00000000000..1f8d9d43ea1 --- /dev/null +++ b/apps/sim/triggers/sim/workspace-event.ts @@ -0,0 +1,172 @@ +import { SimTriggerIcon } from '@/components/icons' +import { fetchWorkspaceWorkflowOptions } from '@/lib/workflows/subblocks/options' +import { + SIM_EVENT_PAYLOAD_FIELDS, + SIM_RULE_DEFAULTS, + SIM_TRIGGER_PROVIDER, + SIM_WORKSPACE_EVENT_TRIGGER_ID, +} from '@/lib/workspace-events/constants' +import type { TriggerConfig } from '@/triggers/types' + +export const simWorkspaceEventTrigger: TriggerConfig = { + id: SIM_WORKSPACE_EVENT_TRIGGER_ID, + name: 'Sim', + provider: SIM_TRIGGER_PROVIDER, + description: + 'Triggers when workspace events occur: execution errors or successes, deployments, and alert conditions like latency or cost spikes', + version: '1.0.0', + icon: SimTriggerIcon, + + subBlocks: [ + { + id: 'eventType', + title: 'Event', + type: 'dropdown', + options: [ + { id: 'execution_error', label: 'Execution Error', group: 'Events' }, + { id: 'execution_success', label: 'Execution Success', group: 'Events' }, + { id: 'workflow_deployed', label: 'Workflow Deployed', group: 'Events' }, + { id: 'consecutive_failures', label: 'Consecutive Failures', group: 'Alert Conditions' }, + { id: 'failure_rate', label: 'Failure Rate', group: 'Alert Conditions' }, + { id: 'latency_threshold', label: 'Latency Threshold', group: 'Alert Conditions' }, + { id: 'latency_spike', label: 'Latency Spike', group: 'Alert Conditions' }, + { id: 'cost_threshold', label: 'Cost Threshold', group: 'Alert Conditions' }, + { id: 'error_count', label: 'Error Count', group: 'Alert Conditions' }, + { id: 'no_activity', label: 'No Activity', group: 'Alert Conditions' }, + ], + defaultValue: 'execution_error', + description: 'The workspace event or alert condition to trigger on.', + required: true, + mode: 'trigger', + }, + { + id: 'workflowIds', + title: 'Workflows', + type: 'dropdown', + multiSelect: true, + options: [], + placeholder: 'All workflows', + description: 'Only fire for these workflows. Leave empty to watch every workflow.', + required: false, + mode: 'trigger', + // A subscriber never receives events about itself, so exclude it. + fetchOptions: () => fetchWorkspaceWorkflowOptions({ excludeActiveWorkflow: true }), + }, + { + id: 'consecutiveFailures', + title: 'Consecutive Failures', + type: 'short-input', + placeholder: String(SIM_RULE_DEFAULTS.consecutiveFailures), + defaultValue: String(SIM_RULE_DEFAULTS.consecutiveFailures), + description: 'Fire after this many consecutive failed executions.', + required: { field: 'eventType', value: 'consecutive_failures' }, + mode: 'trigger', + condition: { field: 'eventType', value: 'consecutive_failures' }, + }, + { + id: 'failureRatePercent', + title: 'Failure Rate (%)', + type: 'short-input', + placeholder: String(SIM_RULE_DEFAULTS.failureRatePercent), + defaultValue: String(SIM_RULE_DEFAULTS.failureRatePercent), + description: + 'Fire when the failure rate meets or exceeds this percentage over the time window.', + required: { field: 'eventType', value: 'failure_rate' }, + mode: 'trigger', + condition: { field: 'eventType', value: 'failure_rate' }, + }, + { + id: 'durationThresholdMs', + title: 'Duration Threshold (ms)', + type: 'short-input', + placeholder: String(SIM_RULE_DEFAULTS.durationThresholdMs), + defaultValue: String(SIM_RULE_DEFAULTS.durationThresholdMs), + description: 'Fire when an execution takes longer than this many milliseconds.', + required: { field: 'eventType', value: 'latency_threshold' }, + mode: 'trigger', + condition: { field: 'eventType', value: 'latency_threshold' }, + }, + { + id: 'latencySpikePercent', + title: 'Latency Spike (%)', + type: 'short-input', + placeholder: String(SIM_RULE_DEFAULTS.latencySpikePercent), + defaultValue: String(SIM_RULE_DEFAULTS.latencySpikePercent), + description: + 'Fire when an execution is this much slower than the average over the time window.', + required: { field: 'eventType', value: 'latency_spike' }, + mode: 'trigger', + condition: { field: 'eventType', value: 'latency_spike' }, + }, + { + id: 'costThresholdCredits', + title: 'Cost Threshold (credits)', + type: 'short-input', + placeholder: String(SIM_RULE_DEFAULTS.costThresholdCredits), + defaultValue: String(SIM_RULE_DEFAULTS.costThresholdCredits), + description: 'Fire when a run costs more than this many credits.', + required: { field: 'eventType', value: 'cost_threshold' }, + mode: 'trigger', + condition: { field: 'eventType', value: 'cost_threshold' }, + }, + { + id: 'errorCountThreshold', + title: 'Error Count', + type: 'short-input', + placeholder: String(SIM_RULE_DEFAULTS.errorCountThreshold), + defaultValue: String(SIM_RULE_DEFAULTS.errorCountThreshold), + description: 'Fire when at least this many errors occur within the time window.', + required: { field: 'eventType', value: 'error_count' }, + mode: 'trigger', + condition: { field: 'eventType', value: 'error_count' }, + }, + { + id: 'windowHours', + title: 'Time Window (hours)', + type: 'short-input', + placeholder: String(SIM_RULE_DEFAULTS.windowHours), + defaultValue: String(SIM_RULE_DEFAULTS.windowHours), + description: 'The rolling time window used to evaluate this condition.', + required: { + field: 'eventType', + value: ['failure_rate', 'latency_spike', 'error_count'], + }, + mode: 'trigger', + condition: { + field: 'eventType', + value: ['failure_rate', 'latency_spike', 'error_count'], + }, + }, + { + id: 'inactivityHours', + title: 'Inactivity Window (hours)', + type: 'short-input', + placeholder: String(SIM_RULE_DEFAULTS.inactivityHours), + defaultValue: String(SIM_RULE_DEFAULTS.inactivityHours), + description: 'Fire when a watched workflow has no executions for this many hours.', + required: { field: 'eventType', value: 'no_activity' }, + mode: 'trigger', + condition: { field: 'eventType', value: 'no_activity' }, + }, + { + id: 'triggerInstructions', + title: 'Setup Instructions', + hideFromPreview: true, + type: 'text', + defaultValue: [ + 'Choose the workspace event or alert condition to react to', + 'Optionally narrow it to specific workflows — leaving the selection empty watches every workflow (this workflow is always excluded; it never triggers itself)', + 'Deploy this workflow — events only fire for deployed workflows', + 'Executions started by this trigger never emit workspace events, so chains and loops are not possible', + ] + .map( + (instruction, index) => + `
${index + 1}. ${instruction}
` + ) + .join(''), + mode: 'trigger', + }, + ], + + outputs: SIM_EVENT_PAYLOAD_FIELDS, +} diff --git a/apps/sim/triggers/types.ts b/apps/sim/triggers/types.ts index 65f0821d355..eb9277484e5 100644 --- a/apps/sim/triggers/types.ts +++ b/apps/sim/triggers/types.ts @@ -1,9 +1,11 @@ -import type { SubBlockConfig } from '@/blocks/types' +import type { OutputCondition, SubBlockConfig } from '@/blocks/types' export interface TriggerOutput { type?: string description?: string | TriggerOutput - [key: string]: TriggerOutput | string | undefined + /** Restricts which trigger configurations surface this output in the tag dropdown. */ + condition?: OutputCondition + [key: string]: TriggerOutput | OutputCondition | string | undefined } export interface TriggerConfig { diff --git a/bun.lock b/bun.lock index 7498ac72abf..6ccdd5bd67f 100644 --- a/bun.lock +++ b/bun.lock @@ -1,6 +1,5 @@ { "lockfileVersion": 1, - "configVersion": 0, "workspaces": { "": { "name": "simstudio", diff --git a/helm/sim/README.md b/helm/sim/README.md index 6bb45abc8d0..4514f0cde7e 100644 --- a/helm/sim/README.md +++ b/helm/sim/README.md @@ -41,7 +41,7 @@ This chart deploys the Sim platform on a Kubernetes cluster using the Helm packa * **`realtime`** — the WebSocket service for live workflow updates (Deployment). * **`postgresql`** — an in-cluster `pgvector/pgvector` Postgres (StatefulSet, with a headless Service for stable per-pod DNS). * **`migrations`** — a Job that applies database migrations on install/upgrade. -* **`cronjobs`** — scheduled jobs for workflow schedule execution, inbox/calendar/drive polling (Gmail, Outlook, Calendar, Drive, Sheets, IMAP, RSS), inactivity alerts, subscription renewal, data drains, and connector syncs. +* **`cronjobs`** — scheduled jobs for workflow schedule execution, inbox/calendar/drive polling (Gmail, Outlook, Calendar, Drive, Sheets, IMAP, RSS), workspace event polling, subscription renewal, data drains, and connector syncs. * **`serviceaccount`** — a dedicated ServiceAccount with `automountServiceAccountToken: false`. Optional components (off by default): diff --git a/helm/sim/values.yaml b/helm/sim/values.yaml index 654549e4af9..4898f925fe9 100644 --- a/helm/sim/values.yaml +++ b/helm/sim/values.yaml @@ -1192,11 +1192,11 @@ cronjobs: successfulJobsHistoryLimit: 3 failedJobsHistoryLimit: 1 - inactivityAlertPoll: + workspaceEventsPoll: enabled: true - name: inactivity-alert-poll + name: workspace-events-poll schedule: "*/15 * * * *" - path: "/api/notifications/poll" + path: "/api/workspace-events/poll" concurrencyPolicy: Forbid successfulJobsHistoryLimit: 3 failedJobsHistoryLimit: 1 diff --git a/packages/audit/src/types.ts b/packages/audit/src/types.ts index 9f238723141..2757f2988f0 100644 --- a/packages/audit/src/types.ts +++ b/packages/audit/src/types.ts @@ -102,11 +102,6 @@ export const AuditAction = { MEMBER_REMOVED: 'member.removed', MEMBER_ROLE_CHANGED: 'member.role_changed', - // Notifications - NOTIFICATION_CREATED: 'notification.created', - NOTIFICATION_UPDATED: 'notification.updated', - NOTIFICATION_DELETED: 'notification.deleted', - // OAuth / Credentials OAUTH_DISCONNECTED: 'oauth.disconnected', CREDENTIAL_CREATED: 'credential.created', @@ -207,7 +202,6 @@ export const AuditResourceType = { FOLDER: 'folder', KNOWLEDGE_BASE: 'knowledge_base', MCP_SERVER: 'mcp_server', - NOTIFICATION: 'notification', OAUTH: 'oauth', ORGANIZATION: 'organization', PASSWORD: 'password', diff --git a/packages/db/migrations/0231_sim_trigger_workspace_events.sql b/packages/db/migrations/0231_sim_trigger_workspace_events.sql new file mode 100644 index 00000000000..57fac45a876 --- /dev/null +++ b/packages/db/migrations/0231_sim_trigger_workspace_events.sql @@ -0,0 +1,14 @@ +CREATE TABLE "sim_trigger_state" ( + "workflow_id" text NOT NULL, + "block_id" text NOT NULL, + "scope_key" text DEFAULT '' NOT NULL, + "last_fired_at" timestamp, + "updated_at" timestamp DEFAULT now() NOT NULL, + CONSTRAINT "sim_trigger_state_workflow_id_block_id_scope_key_pk" PRIMARY KEY("workflow_id","block_id","scope_key") +); +--> statement-breakpoint +DROP TABLE "workspace_notification_delivery" CASCADE;--> statement-breakpoint +DROP TABLE "workspace_notification_subscription" CASCADE;--> statement-breakpoint +ALTER TABLE "sim_trigger_state" ADD CONSTRAINT "sim_trigger_state_workflow_id_workflow_id_fk" FOREIGN KEY ("workflow_id") REFERENCES "public"."workflow"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +DROP TYPE "public"."notification_delivery_status";--> statement-breakpoint +DROP TYPE "public"."notification_type"; \ No newline at end of file diff --git a/packages/db/migrations/meta/0231_snapshot.json b/packages/db/migrations/meta/0231_snapshot.json new file mode 100644 index 00000000000..9e1ba095a47 --- /dev/null +++ b/packages/db/migrations/meta/0231_snapshot.json @@ -0,0 +1,16266 @@ +{ + "id": "4c727ad1-ff15-463f-a929-61b0c74b4e4d", + "prevId": "b6675160-a7cc-4e1b-bbc3-4b050284b789", + "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 + }, + "archived_at": { + "name": "archived_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_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, + "where": "\"a2a_agent\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_agent_archived_at_idx": { + "name": "a2a_agent_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_agent_workspace_archived_partial_idx": { + "name": "a2a_agent_workspace_archived_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"a2a_agent\".\"archived_at\" IS NOT NULL", + "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_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.academy_certificate": { + "name": "academy_certificate", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "course_id": { + "name": "course_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "academy_cert_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "issued_at": { + "name": "issued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "certificate_number": { + "name": "certificate_number", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "academy_certificate_user_id_idx": { + "name": "academy_certificate_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_course_id_idx": { + "name": "academy_certificate_course_id_idx", + "columns": [ + { + "expression": "course_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_user_course_unique": { + "name": "academy_certificate_user_course_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "course_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_number_idx": { + "name": "academy_certificate_number_idx", + "columns": [ + { + "expression": "certificate_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_status_idx": { + "name": "academy_certificate_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "academy_certificate_user_id_user_id_fk": { + "name": "academy_certificate_user_id_user_id_fk", + "tableFrom": "academy_certificate", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "academy_certificate_certificate_number_unique": { + "name": "academy_certificate_certificate_number_unique", + "nullsNotDistinct": false, + "columns": ["certificate_number"] + } + }, + "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": {} + } + }, + "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 + }, + "key_hash": { + "name": "key_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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": {} + }, + "api_key_key_hash_idx": { + "name": "api_key_key_hash_idx", + "columns": [ + { + "expression": "key_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "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.async_jobs": { + "name": "async_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "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()" + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "run_at": { + "name": "run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_attempts": { + "name": "max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 3 + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "output": { + "name": "output", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "async_jobs_status_started_at_idx": { + "name": "async_jobs_status_started_at_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_status_completed_at_idx": { + "name": "async_jobs_status_completed_at_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "completed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_schedule_pending_run_at_idx": { + "name": "async_jobs_schedule_pending_run_at_idx", + "columns": [ + { + "expression": "run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"async_jobs\".\"type\" = 'schedule-execution' AND \"async_jobs\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_schedule_processing_started_at_idx": { + "name": "async_jobs_schedule_processing_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"async_jobs\".\"type\" = 'schedule-execution' AND \"async_jobs\".\"status\" = 'processing'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_log": { + "name": "audit_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_name": { + "name": "actor_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_email": { + "name": "actor_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "audit_log_workspace_created_idx": { + "name": "audit_log_workspace_created_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_workspace_created_at_id_idx": { + "name": "audit_log_workspace_created_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"created_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_actor_created_idx": { + "name": "audit_log_actor_created_idx", + "columns": [ + { + "expression": "actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_resource_idx": { + "name": "audit_log_resource_idx", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_action_idx": { + "name": "audit_log_action_idx", + "columns": [ + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audit_log_workspace_id_workspace_id_fk": { + "name": "audit_log_workspace_id_workspace_id_fk", + "tableFrom": "audit_log", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "audit_log_actor_id_user_id_fk": { + "name": "audit_log_actor_id_user_id_fk", + "tableFrom": "audit_log", + "tableTo": "user", + "columnsFrom": ["actor_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "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": "'[]'" + }, + "archived_at": { + "name": "archived_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": { + "identifier_idx": { + "name": "identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"chat\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "chat_archived_at_partial_idx": { + "name": "chat_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"chat\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_chat_on_workflow_id_archived_at": { + "name": "idx_chat_on_workflow_id_archived_at", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "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_async_tool_calls": { + "name": "copilot_async_tool_calls", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "checkpoint_id": { + "name": "checkpoint_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "args": { + "name": "args", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "status": { + "name": "status", + "type": "copilot_async_tool_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "claimed_by": { + "name": "claimed_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_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": { + "copilot_async_tool_calls_run_id_idx": { + "name": "copilot_async_tool_calls_run_id_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_checkpoint_id_idx": { + "name": "copilot_async_tool_calls_checkpoint_id_idx", + "columns": [ + { + "expression": "checkpoint_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_tool_call_id_idx": { + "name": "copilot_async_tool_calls_tool_call_id_idx", + "columns": [ + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_status_idx": { + "name": "copilot_async_tool_calls_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_run_status_idx": { + "name": "copilot_async_tool_calls_run_status_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_tool_call_id_unique": { + "name": "copilot_async_tool_calls_tool_call_id_unique", + "columns": [ + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_async_tool_calls_run_id_copilot_runs_id_fk": { + "name": "copilot_async_tool_calls_run_id_copilot_runs_id_fk", + "tableFrom": "copilot_async_tool_calls", + "tableTo": "copilot_runs", + "columnsFrom": ["run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_async_tool_calls_checkpoint_id_copilot_run_checkpoints_id_fk": { + "name": "copilot_async_tool_calls_checkpoint_id_copilot_run_checkpoints_id_fk", + "tableFrom": "copilot_async_tool_calls", + "tableTo": "copilot_run_checkpoints", + "columnsFrom": ["checkpoint_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": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "chat_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'copilot'" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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 + }, + "resources": { + "name": "resources", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "pinned": { + "name": "pinned", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": 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_user_workspace_idx": { + "name": "copilot_chats_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": {} + }, + "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": {} + }, + "copilot_chats_workspace_created_at_id_idx": { + "name": "copilot_chats_workspace_created_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"created_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "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" + }, + "copilot_chats_workspace_id_workspace_id_fk": { + "name": "copilot_chats_workspace_id_workspace_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "workspace", + "columnsFrom": ["workspace_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.copilot_messages": { + "name": "copilot_messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "stream_id": { + "name": "stream_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parent_message_id": { + "name": "parent_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tokens_in": { + "name": "tokens_in", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "tokens_out": { + "name": "tokens_out", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "seq": { + "name": "seq", + "type": "integer", + "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": { + "copilot_messages_chat_message_unique": { + "name": "copilot_messages_chat_message_unique", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_chat_created_at_idx": { + "name": "copilot_messages_chat_created_at_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_chat_seq_idx": { + "name": "copilot_messages_chat_seq_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "seq", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_chat_stream_idx": { + "name": "copilot_messages_chat_stream_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "stream_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"stream_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_messages_chat_id_copilot_chats_id_fk": { + "name": "copilot_messages_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_messages", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_run_checkpoints": { + "name": "copilot_run_checkpoints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "pending_tool_call_id": { + "name": "pending_tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "conversation_snapshot": { + "name": "conversation_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "agent_state": { + "name": "agent_state", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "provider_request": { + "name": "provider_request", + "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": { + "copilot_run_checkpoints_run_id_idx": { + "name": "copilot_run_checkpoints_run_id_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_run_checkpoints_pending_tool_call_id_idx": { + "name": "copilot_run_checkpoints_pending_tool_call_id_idx", + "columns": [ + { + "expression": "pending_tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_run_checkpoints_run_pending_tool_unique": { + "name": "copilot_run_checkpoints_run_pending_tool_unique", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pending_tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_run_checkpoints_run_id_copilot_runs_id_fk": { + "name": "copilot_run_checkpoints_run_id_copilot_runs_id_fk", + "tableFrom": "copilot_run_checkpoints", + "tableTo": "copilot_runs", + "columnsFrom": ["run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_runs": { + "name": "copilot_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_run_id": { + "name": "parent_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stream_id": { + "name": "stream_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent": { + "name": "agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "copilot_run_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "request_context": { + "name": "request_context", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_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()" + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "copilot_runs_execution_id_idx": { + "name": "copilot_runs_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_parent_run_id_idx": { + "name": "copilot_runs_parent_run_id_idx", + "columns": [ + { + "expression": "parent_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_chat_id_idx": { + "name": "copilot_runs_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_user_id_idx": { + "name": "copilot_runs_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_workflow_id_idx": { + "name": "copilot_runs_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_workspace_id_idx": { + "name": "copilot_runs_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_status_idx": { + "name": "copilot_runs_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_chat_execution_idx": { + "name": "copilot_runs_chat_execution_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_execution_started_at_idx": { + "name": "copilot_runs_execution_started_at_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_workspace_completed_at_id_idx": { + "name": "copilot_runs_workspace_completed_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"completed_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_stream_id_unique": { + "name": "copilot_runs_stream_id_unique", + "columns": [ + { + "expression": "stream_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_runs_chat_id_copilot_chats_id_fk": { + "name": "copilot_runs_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_user_id_user_id_fk": { + "name": "copilot_runs_user_id_user_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_workflow_id_workflow_id_fk": { + "name": "copilot_runs_workflow_id_workflow_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_workspace_id_workspace_id_fk": { + "name": "copilot_runs_workspace_id_workspace_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_workflow_read_hashes": { + "name": "copilot_workflow_read_hashes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hash": { + "name": "hash", + "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": { + "copilot_workflow_read_hashes_chat_id_idx": { + "name": "copilot_workflow_read_hashes_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_workflow_read_hashes_workflow_id_idx": { + "name": "copilot_workflow_read_hashes_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_workflow_read_hashes_chat_workflow_unique": { + "name": "copilot_workflow_read_hashes_chat_workflow_unique", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_workflow_read_hashes_chat_id_copilot_chats_id_fk": { + "name": "copilot_workflow_read_hashes_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_workflow_read_hashes", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_workflow_read_hashes_workflow_id_workflow_id_fk": { + "name": "copilot_workflow_read_hashes_workflow_id_workflow_id_fk", + "tableFrom": "copilot_workflow_read_hashes", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential": { + "name": "credential", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "credential_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_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": false + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env_key": { + "name": "env_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env_owner_user_id": { + "name": "env_owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_service_account_key": { + "name": "encrypted_service_account_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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_workspace_id_idx": { + "name": "credential_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_type_idx": { + "name": "credential_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_provider_id_idx": { + "name": "credential_provider_id_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_account_id_idx": { + "name": "credential_account_id_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_env_owner_user_id_idx": { + "name": "credential_env_owner_user_id_idx", + "columns": [ + { + "expression": "env_owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_account_unique": { + "name": "credential_workspace_account_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "account_id IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_env_unique": { + "name": "credential_workspace_env_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "type = 'env_workspace'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_personal_env_unique": { + "name": "credential_workspace_personal_env_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env_owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "type = 'env_personal'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_workspace_id_workspace_id_fk": { + "name": "credential_workspace_id_workspace_id_fk", + "tableFrom": "credential", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_account_id_account_id_fk": { + "name": "credential_account_id_account_id_fk", + "tableFrom": "credential", + "tableTo": "account", + "columnsFrom": ["account_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_env_owner_user_id_user_id_fk": { + "name": "credential_env_owner_user_id_user_id_fk", + "tableFrom": "credential", + "tableTo": "user", + "columnsFrom": ["env_owner_user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_created_by_user_id_fk": { + "name": "credential_created_by_user_id_fk", + "tableFrom": "credential", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "credential_oauth_source_check": { + "name": "credential_oauth_source_check", + "value": "(type <> 'oauth') OR (account_id IS NOT NULL AND provider_id IS NOT NULL)" + }, + "credential_workspace_env_source_check": { + "name": "credential_workspace_env_source_check", + "value": "(type <> 'env_workspace') OR (env_key IS NOT NULL AND env_owner_user_id IS NULL)" + }, + "credential_personal_env_source_check": { + "name": "credential_personal_env_source_check", + "value": "(type <> 'env_personal') OR (env_key IS NOT NULL AND env_owner_user_id IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.credential_member": { + "name": "credential_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "credential_member_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "status": { + "name": "status", + "type": "credential_member_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "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_member_user_id_idx": { + "name": "credential_member_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_role_idx": { + "name": "credential_member_role_idx", + "columns": [ + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_status_idx": { + "name": "credential_member_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_unique": { + "name": "credential_member_unique", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_member_credential_id_credential_id_fk": { + "name": "credential_member_credential_id_credential_id_fk", + "tableFrom": "credential_member", + "tableTo": "credential", + "columnsFrom": ["credential_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_member_user_id_user_id_fk": { + "name": "credential_member_user_id_user_id_fk", + "tableFrom": "credential_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_member_invited_by_user_id_fk": { + "name": "credential_member_invited_by_user_id_fk", + "tableFrom": "credential_member", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "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_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_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.data_drain_runs": { + "name": "data_drain_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "drain_id": { + "name": "drain_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "data_drain_run_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "trigger": { + "name": "trigger", + "type": "data_drain_run_trigger", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "rows_exported": { + "name": "rows_exported", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "bytes_written": { + "name": "bytes_written", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cursor_before": { + "name": "cursor_before", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cursor_after": { + "name": "cursor_after", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "locators": { + "name": "locators", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + } + }, + "indexes": { + "data_drain_runs_drain_started_idx": { + "name": "data_drain_runs_drain_started_idx", + "columns": [ + { + "expression": "drain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "data_drain_runs_drain_id_data_drains_id_fk": { + "name": "data_drain_runs_drain_id_data_drains_id_fk", + "tableFrom": "data_drain_runs", + "tableTo": "data_drains", + "columnsFrom": ["drain_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.data_drains": { + "name": "data_drains", + "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 + }, + "source": { + "name": "source", + "type": "data_drain_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "destination_type": { + "name": "destination_type", + "type": "data_drain_destination", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "destination_config": { + "name": "destination_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "destination_credentials": { + "name": "destination_credentials", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schedule_cadence": { + "name": "schedule_cadence", + "type": "data_drain_cadence", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "cursor": { + "name": "cursor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_success_at": { + "name": "last_success_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "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": { + "data_drains_org_idx": { + "name": "data_drains_org_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "data_drains_due_idx": { + "name": "data_drains_due_idx", + "columns": [ + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "data_drains_org_name_unique": { + "name": "data_drains_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": { + "data_drains_organization_id_organization_id_fk": { + "name": "data_drains_organization_id_organization_id_fk", + "tableFrom": "data_drains", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "data_drains_created_by_user_id_fk": { + "name": "data_drains_created_by_user_id_fk", + "tableFrom": "data_drains", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "no action", + "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 + }, + "storage_key": { + "name": "storage_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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 + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "user_excluded": { + "name": "user_excluded", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": 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 + }, + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_url": { + "name": "source_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uploaded_by": { + "name": "uploaded_by", + "type": "text", + "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_connector_external_id_idx": { + "name": "doc_connector_external_id_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"document\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_connector_id_idx": { + "name": "doc_connector_id_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_storage_key_idx": { + "name": "doc_storage_key_idx", + "columns": [ + { + "expression": "storage_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"storage_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_archived_at_partial_idx": { + "name": "doc_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_deleted_at_partial_idx": { + "name": "doc_deleted_at_partial_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"deleted_at\" IS NOT NULL", + "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" + }, + "document_connector_id_knowledge_connector_id_fk": { + "name": "document_connector_id_knowledge_connector_id_fk", + "tableFrom": "document", + "tableTo": "knowledge_connector", + "columnsFrom": ["connector_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "document_uploaded_by_user_id_fk": { + "name": "document_uploaded_by_user_id_fk", + "tableFrom": "document", + "tableTo": "user", + "columnsFrom": ["uploaded_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "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.execution_large_value_dependencies": { + "name": "execution_large_value_dependencies", + "schema": "", + "columns": { + "parent_key": { + "name": "parent_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "child_key": { + "name": "child_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "execution_large_value_dependencies_workspace_parent_key_idx": { + "name": "execution_large_value_dependencies_workspace_parent_key_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_value_dependencies_workspace_child_key_idx": { + "name": "execution_large_value_dependencies_workspace_child_key_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "child_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "execution_large_value_dependencies_workspace_id_workspace_id_fk": { + "name": "execution_large_value_dependencies_workspace_id_workspace_id_fk", + "tableFrom": "execution_large_value_dependencies", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "execution_large_value_dependencies_parent_key_child_key_pk": { + "name": "execution_large_value_dependencies_parent_key_child_key_pk", + "columns": ["parent_key", "child_key"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.execution_large_value_references": { + "name": "execution_large_value_references", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "execution_large_value_reference_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "execution_large_value_references_workspace_execution_source_idx": { + "name": "execution_large_value_references_workspace_execution_source_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "execution_large_value_references_workspace_id_workspace_id_fk": { + "name": "execution_large_value_references_workspace_id_workspace_id_fk", + "tableFrom": "execution_large_value_references", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "execution_large_value_references_workflow_id_workflow_id_fk": { + "name": "execution_large_value_references_workflow_id_workflow_id_fk", + "tableFrom": "execution_large_value_references", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "execution_large_value_references_key_execution_id_source_pk": { + "name": "execution_large_value_references_key_execution_id_source_pk", + "columns": ["key", "execution_id", "source"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.execution_large_values": { + "name": "execution_large_values", + "schema": "", + "columns": { + "key": { + "name": "key", + "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": false + }, + "owner_execution_id": { + "name": "owner_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "execution_large_values_owner_execution_id_idx": { + "name": "execution_large_values_owner_execution_id_idx", + "columns": [ + { + "expression": "owner_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_values_cleanup_idx": { + "name": "execution_large_values_cleanup_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"execution_large_values\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_values_tombstone_cleanup_idx": { + "name": "execution_large_values_tombstone_cleanup_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"execution_large_values\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "execution_large_values_workspace_id_workspace_id_fk": { + "name": "execution_large_values_workspace_id_workspace_id_fk", + "tableFrom": "execution_large_values", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "execution_large_values_workflow_id_workflow_id_fk": { + "name": "execution_large_values_workflow_id_workflow_id_fk", + "tableFrom": "execution_large_values", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.idempotency_key": { + "name": "idempotency_key", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "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_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": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation": { + "name": "invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "invitation_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'organization'" + }, + "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": false + }, + "membership_intent": { + "name": "membership_intent", + "type": "invitation_membership_intent", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'internal'" + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "invitation_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "token": { + "name": "token", + "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()" + }, + "updated_at": { + "name": "updated_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": {} + }, + "invitation_status_idx": { + "name": "invitation_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_pending_email_org_unique": { + "name": "invitation_pending_email_org_unique", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"invitation\".\"status\" = 'pending' AND \"invitation\".\"organization_id\" IS NOT NULL", + "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": { + "invitation_token_unique": { + "name": "invitation_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation_workspace_grant": { + "name": "invitation_workspace_grant", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "invitation_id": { + "name": "invitation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission": { + "name": "permission", + "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": { + "invitation_workspace_grant_unique": { + "name": "invitation_workspace_grant_unique", + "columns": [ + { + "expression": "invitation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_workspace_grant_workspace_id_idx": { + "name": "invitation_workspace_grant_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invitation_workspace_grant_invitation_id_invitation_id_fk": { + "name": "invitation_workspace_grant_invitation_id_invitation_id_fk", + "tableFrom": "invitation_workspace_grant", + "tableTo": "invitation", + "columnsFrom": ["invitation_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_workspace_grant_workspace_id_workspace_id_fk": { + "name": "invitation_workspace_grant_workspace_id_workspace_id_fk", + "tableFrom": "invitation_workspace_grant", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.job_execution_logs": { + "name": "job_execution_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "schedule_id": { + "name": "schedule_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "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 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "job_execution_logs_schedule_id_idx": { + "name": "job_execution_logs_schedule_id_idx", + "columns": [ + { + "expression": "schedule_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_workspace_started_at_idx": { + "name": "job_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": {} + }, + "job_execution_logs_workspace_ended_at_id_idx": { + "name": "job_execution_logs_workspace_ended_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"ended_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_execution_id_unique": { + "name": "job_execution_logs_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_trigger_idx": { + "name": "job_execution_logs_trigger_idx", + "columns": [ + { + "expression": "trigger", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "job_execution_logs_schedule_id_workflow_schedule_id_fk": { + "name": "job_execution_logs_schedule_id_workflow_schedule_id_fk", + "tableFrom": "job_execution_logs", + "tableTo": "workflow_schedule", + "columnsFrom": ["schedule_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "job_execution_logs_workspace_id_workspace_id_fk": { + "name": "job_execution_logs_workspace_id_workspace_id_fk", + "tableFrom": "job_execution_logs", + "tableTo": "workspace", + "columnsFrom": ["workspace_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": {} + }, + "kb_workspace_deleted_partial_idx": { + "name": "kb_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_base\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_name_active_unique": { + "name": "kb_workspace_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"knowledge_base\".\"deleted_at\" IS NULL", + "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.knowledge_connector": { + "name": "knowledge_connector", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connector_type": { + "name": "connector_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_config": { + "name": "source_config", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "sync_mode": { + "name": "sync_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'full'" + }, + "sync_interval_minutes": { + "name": "sync_interval_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1440 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_sync_at": { + "name": "last_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_sync_error": { + "name": "last_sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_sync_doc_count": { + "name": "last_sync_doc_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "next_sync_at": { + "name": "next_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "consecutive_failures": { + "name": "consecutive_failures", + "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()" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "kc_knowledge_base_id_idx": { + "name": "kc_knowledge_base_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_status_next_sync_idx": { + "name": "kc_status_next_sync_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_sync_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_archived_at_partial_idx": { + "name": "kc_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_deleted_at_partial_idx": { + "name": "kc_deleted_at_partial_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_connector_knowledge_base_id_knowledge_base_id_fk": { + "name": "knowledge_connector_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "knowledge_connector", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_connector_sync_log": { + "name": "knowledge_connector_sync_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "docs_added": { + "name": "docs_added", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_updated": { + "name": "docs_updated", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_deleted": { + "name": "docs_deleted", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_unchanged": { + "name": "docs_unchanged", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_failed": { + "name": "docs_failed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "kcsl_connector_id_idx": { + "name": "kcsl_connector_id_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_connector_sync_log_connector_id_knowledge_connector_id_fk": { + "name": "knowledge_connector_sync_log_connector_id_knowledge_connector_id_fk", + "tableFrom": "knowledge_connector_sync_log", + "tableTo": "knowledge_connector", + "columnsFrom": ["connector_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_server_oauth": { + "name": "mcp_server_oauth", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "mcp_server_id": { + "name": "mcp_server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_information": { + "name": "client_information", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tokens": { + "name": "tokens", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "code_verifier": { + "name": "code_verifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state_created_at": { + "name": "state_created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_refreshed_at": { + "name": "last_refreshed_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_server_oauth_server_unique": { + "name": "mcp_server_oauth_server_unique", + "columns": [ + { + "expression": "mcp_server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_server_oauth_state_idx": { + "name": "mcp_server_oauth_state_idx", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_server_oauth_mcp_server_id_mcp_servers_id_fk": { + "name": "mcp_server_oauth_mcp_server_id_mcp_servers_id_fk", + "tableFrom": "mcp_server_oauth", + "tableTo": "mcp_servers", + "columnsFrom": ["mcp_server_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_server_oauth_user_id_user_id_fk": { + "name": "mcp_server_oauth_user_id_user_id_fk", + "tableFrom": "mcp_server_oauth", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "mcp_server_oauth_workspace_id_workspace_id_fk": { + "name": "mcp_server_oauth_workspace_id_workspace_id_fk", + "tableFrom": "mcp_server_oauth", + "tableTo": "workspace", + "columnsFrom": ["workspace_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 + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'headers'" + }, + "oauth_client_id": { + "name": "oauth_client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oauth_client_secret": { + "name": "oauth_client_secret", + "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_partial_idx": { + "name": "mcp_servers_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"mcp_servers\".\"deleted_at\" IS NOT NULL", + "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": {} + }, + "memory_workspace_deleted_partial_idx": { + "name": "memory_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"memory\".\"deleted_at\" IS NOT NULL", + "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.mothership_inbox_allowed_sender": { + "name": "mothership_inbox_allowed_sender", + "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 + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "added_by": { + "name": "added_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "inbox_sender_ws_email_idx": { + "name": "inbox_sender_ws_email_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mothership_inbox_allowed_sender_workspace_id_workspace_id_fk": { + "name": "mothership_inbox_allowed_sender_workspace_id_workspace_id_fk", + "tableFrom": "mothership_inbox_allowed_sender", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mothership_inbox_allowed_sender_added_by_user_id_fk": { + "name": "mothership_inbox_allowed_sender_added_by_user_id_fk", + "tableFrom": "mothership_inbox_allowed_sender", + "tableTo": "user", + "columnsFrom": ["added_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_inbox_task": { + "name": "mothership_inbox_task", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "from_email": { + "name": "from_email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "from_name": { + "name": "from_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body_preview": { + "name": "body_preview", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body_text": { + "name": "body_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body_html": { + "name": "body_html", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_message_id": { + "name": "email_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "in_reply_to": { + "name": "in_reply_to", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "response_message_id": { + "name": "response_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agentmail_message_id": { + "name": "agentmail_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'received'" + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "trigger_job_id": { + "name": "trigger_job_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result_summary": { + "name": "result_summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rejection_reason": { + "name": "rejection_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "has_attachments": { + "name": "has_attachments", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "cc_recipients": { + "name": "cc_recipients", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "processing_started_at": { + "name": "processing_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "inbox_task_ws_created_at_idx": { + "name": "inbox_task_ws_created_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_task_ws_status_idx": { + "name": "inbox_task_ws_status_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_task_response_msg_id_idx": { + "name": "inbox_task_response_msg_id_idx", + "columns": [ + { + "expression": "response_message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_task_email_msg_id_idx": { + "name": "inbox_task_email_msg_id_idx", + "columns": [ + { + "expression": "email_message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mothership_inbox_task_workspace_id_workspace_id_fk": { + "name": "mothership_inbox_task_workspace_id_workspace_id_fk", + "tableFrom": "mothership_inbox_task", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mothership_inbox_task_chat_id_copilot_chats_id_fk": { + "name": "mothership_inbox_task_chat_id_copilot_chats_id_fk", + "tableFrom": "mothership_inbox_task", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_inbox_webhook": { + "name": "mothership_inbox_webhook", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "webhook_id": { + "name": "webhook_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "mothership_inbox_webhook_workspace_id_workspace_id_fk": { + "name": "mothership_inbox_webhook_workspace_id_workspace_id_fk", + "tableFrom": "mothership_inbox_webhook", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mothership_inbox_webhook_workspace_id_unique": { + "name": "mothership_inbox_webhook_workspace_id_unique", + "nullsNotDistinct": false, + "columns": ["workspace_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_settings": { + "name": "mothership_settings", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "mcp_tool_refs": { + "name": "mcp_tool_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "custom_tool_refs": { + "name": "custom_tool_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "skill_refs": { + "name": "skill_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "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": { + "mothership_settings_workspace_id_idx": { + "name": "mothership_settings_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mothership_settings_workspace_id_workspace_id_fk": { + "name": "mothership_settings_workspace_id_workspace_id_fk", + "tableFrom": "mothership_settings", + "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 + }, + "whitelabel_settings": { + "name": "whitelabel_settings", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "data_retention_settings": { + "name": "data_retention_settings", + "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.organization_member_usage_limit": { + "name": "organization_member_usage_limit", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "usage_limit": { + "name": "usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "set_by": { + "name": "set_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": { + "org_member_usage_limit_org_user_unique": { + "name": "org_member_usage_limit_org_user_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "org_member_usage_limit_organization_id_idx": { + "name": "org_member_usage_limit_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organization_member_usage_limit_organization_id_organization_id_fk": { + "name": "organization_member_usage_limit_organization_id_organization_id_fk", + "tableFrom": "organization_member_usage_limit", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_member_usage_limit_user_id_user_id_fk": { + "name": "organization_member_usage_limit_user_id_user_id_fk", + "tableFrom": "organization_member_usage_limit", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_member_usage_limit_set_by_user_id_fk": { + "name": "organization_member_usage_limit_set_by_user_id_fk", + "tableFrom": "organization_member_usage_limit", + "tableTo": "user", + "columnsFrom": ["set_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.outbox_event": { + "name": "outbox_event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_attempts": { + "name": "max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10 + }, + "available_at": { + "name": "available_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "locked_at": { + "name": "locked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "outbox_event_status_available_idx": { + "name": "outbox_event_status_available_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "available_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "outbox_event_locked_at_idx": { + "name": "outbox_event_locked_at_idx", + "columns": [ + { + "expression": "locked_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "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 + }, + "next_resume_at": { + "name": "next_resume_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": {} + }, + "paused_executions_next_resume_at_idx": { + "name": "paused_executions_next_resume_at_idx", + "columns": [ + { + "expression": "next_resume_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "status = 'paused' AND next_resume_at IS NOT NULL", + "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.pending_credential_draft": { + "name": "pending_credential_draft", + "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": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_id": { + "name": "credential_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()" + } + }, + "indexes": { + "pending_draft_user_provider_ws": { + "name": "pending_draft_user_provider_ws", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pending_credential_draft_user_id_user_id_fk": { + "name": "pending_credential_draft_user_id_user_id_fk", + "tableFrom": "pending_credential_draft", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pending_credential_draft_workspace_id_workspace_id_fk": { + "name": "pending_credential_draft_workspace_id_workspace_id_fk", + "tableFrom": "pending_credential_draft", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pending_credential_draft_credential_id_credential_id_fk": { + "name": "pending_credential_draft_credential_id_credential_id_fk", + "tableFrom": "pending_credential_draft", + "tableTo": "credential", + "columnsFrom": ["credential_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 + }, + "workspace_id": { + "name": "workspace_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()" + }, + "auto_add_new_members": { + "name": "auto_add_new_members", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "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_workspace_name_unique": { + "name": "permission_group_workspace_name_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_workspace_auto_add_unique": { + "name": "permission_group_workspace_auto_add_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "auto_add_new_members = true", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_workspace_id_workspace_id_fk": { + "name": "permission_group_workspace_id_workspace_id_fk", + "tableFrom": "permission_group", + "tableTo": "workspace", + "columnsFrom": ["workspace_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 + }, + "workspace_id": { + "name": "workspace_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_group_user_unique": { + "name": "permission_group_member_group_user_unique", + "columns": [ + { + "expression": "permission_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_member_workspace_user_unique": { + "name": "permission_group_member_workspace_user_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "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_workspace_id_workspace_id_fk": { + "name": "permission_group_member_workspace_id_workspace_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "workspace", + "columnsFrom": ["workspace_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 + }, + "impersonated_by": { + "name": "impersonated_by", + "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 + }, + "mothership_environment": { + "name": "mothership_environment", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "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 + }, + "show_action_bar": { + "name": "show_action_bar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "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": "'[]'" + }, + "last_active_workspace_id": { + "name": "last_active_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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.sim_trigger_state": { + "name": "sim_trigger_state", + "schema": "", + "columns": { + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_key": { + "name": "scope_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "last_fired_at": { + "name": "last_fired_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "sim_trigger_state_workflow_id_workflow_id_fk": { + "name": "sim_trigger_state_workflow_id_workflow_id_fk", + "tableFrom": "sim_trigger_state", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sim_trigger_state_workflow_id_block_id_scope_key_pk": { + "name": "sim_trigger_state_workflow_id_block_id_scope_key_pk", + "columns": ["workflow_id", "block_id", "scope_key"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.skill": { + "name": "skill", + "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 + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "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": { + "skill_workspace_name_unique": { + "name": "skill_workspace_name_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "skill_workspace_id_workspace_id_fk": { + "name": "skill_workspace_id_workspace_id_fk", + "tableFrom": "skill", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "skill_user_id_user_id_fk": { + "name": "skill_user_id_user_id_fk", + "tableFrom": "skill", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "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 + }, + "cancel_at": { + "name": "cancel_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "canceled_at": { + "name": "canceled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "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 + }, + "billing_interval": { + "name": "billing_interval", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_schedule_id": { + "name": "stripe_schedule_id", + "type": "text", + "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.table_row_executions": { + "name": "table_row_executions", + "schema": "", + "columns": { + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "row_id": { + "name": "row_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "job_id": { + "name": "job_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "running_block_ids": { + "name": "running_block_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "block_errors": { + "name": "block_errors", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "table_row_executions_table_status_idx": { + "name": "table_row_executions_table_status_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"table_row_executions\".\"status\" IN ('queued', 'running', 'pending')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_row_executions_execution_id_idx": { + "name": "table_row_executions_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"table_row_executions\".\"execution_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_row_executions_table_group_idx": { + "name": "table_row_executions_table_group_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "table_row_executions_table_id_user_table_definitions_id_fk": { + "name": "table_row_executions_table_id_user_table_definitions_id_fk", + "tableFrom": "table_row_executions", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_row_executions_row_id_user_table_rows_id_fk": { + "name": "table_row_executions_row_id_user_table_rows_id_fk", + "tableFrom": "table_row_executions", + "tableTo": "user_table_rows", + "columnsFrom": ["row_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "table_row_executions_row_id_group_id_pk": { + "name": "table_row_executions_row_id_group_id_pk", + "columns": ["row_id", "group_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.table_run_dispatches": { + "name": "table_run_dispatches", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "cursor": { + "name": "cursor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "limit": { + "name": "limit", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "processed_count": { + "name": "processed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "is_manual_run": { + "name": "is_manual_run", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "triggered_by_user_id": { + "name": "triggered_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "table_run_dispatches_active_idx": { + "name": "table_run_dispatches_active_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_run_dispatches_watchdog_idx": { + "name": "table_run_dispatches_watchdog_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "requested_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "table_run_dispatches_table_id_user_table_definitions_id_fk": { + "name": "table_run_dispatches_table_id_user_table_definitions_id_fk", + "tableFrom": "table_run_dispatches", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_run_dispatches_workspace_id_workspace_id_fk": { + "name": "table_run_dispatches_workspace_id_workspace_id_fk", + "tableFrom": "table_run_dispatches", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_run_dispatches_triggered_by_user_id_user_id_fk": { + "name": "table_run_dispatches_triggered_by_user_id_user_id_fk", + "tableFrom": "table_run_dispatches", + "tableTo": "user", + "columnsFrom": ["triggered_by_user_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 + }, + "event_key": { + "name": "event_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "billing_entity_type": { + "name": "billing_entity_type", + "type": "billing_entity_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "billing_entity_id": { + "name": "billing_entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "billing_period_start": { + "name": "billing_period_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "billing_period_end": { + "name": "billing_period_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "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": {} + }, + "usage_log_event_key_unique": { + "name": "usage_log_event_key_unique", + "columns": [ + { + "expression": "event_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"usage_log\".\"event_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_billing_entity_period_idx": { + "name": "usage_log_billing_entity_period_idx", + "columns": [ + { + "expression": "billing_entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_period_start", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_period_end", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_log\".\"billing_entity_type\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workspace_created_at_idx": { + "name": "usage_log_workspace_created_at_idx", + "columns": [ + { + "expression": "workspace_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_execution_id_idx": { + "name": "usage_log_execution_id_idx", + "columns": [ + { + "expression": "execution_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": { + "usage_log_billing_scope_all_or_none": { + "name": "usage_log_billing_scope_all_or_none", + "value": "(\n (\"usage_log\".\"billing_entity_type\" IS NULL AND \"usage_log\".\"billing_entity_id\" IS NULL AND \"usage_log\".\"billing_period_start\" IS NULL AND \"usage_log\".\"billing_period_end\" IS NULL)\n OR\n (\"usage_log\".\"billing_entity_type\" IS NOT NULL AND \"usage_log\".\"billing_entity_id\" IS NOT NULL AND \"usage_log\".\"billing_period_start\" IS NOT NULL AND \"usage_log\".\"billing_period_end\" IS NOT NULL AND \"usage_log\".\"billing_period_start\" < \"usage_log\".\"billing_period_end\")\n )" + } + }, + "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 + }, + "normalized_email": { + "name": "normalized_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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 + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'user'" + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + }, + "user_normalized_email_unique": { + "name": "user_normalized_email_unique", + "nullsNotDistinct": false, + "columns": ["normalized_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_mcp_executions": { + "name": "total_mcp_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_a2a_executions": { + "name": "total_a2a_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_tokens_used": { + "name": "total_tokens_used", + "type": "bigint", + "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": "'5'" + }, + "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'" + }, + "pro_period_cost_snapshot_at": { + "name": "pro_period_cost_snapshot_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "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": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_copilot_calls": { + "name": "total_copilot_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_mcp_copilot_calls": { + "name": "total_mcp_copilot_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_mcp_copilot_cost": { + "name": "total_mcp_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_period_mcp_copilot_cost": { + "name": "current_period_mcp_copilot_cost", + "type": "numeric", + "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.user_table_definitions": { + "name": "user_table_definitions", + "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 + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "schema": { + "name": "schema", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "max_rows": { + "name": "max_rows", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10000 + }, + "row_count": { + "name": "row_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "import_status": { + "name": "import_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "import_id": { + "name": "import_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "import_error": { + "name": "import_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "import_rows_processed": { + "name": "import_rows_processed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "import_started_at": { + "name": "import_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "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": { + "user_table_def_workspace_id_idx": { + "name": "user_table_def_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_workspace_name_unique": { + "name": "user_table_def_workspace_name_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"user_table_definitions\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_archived_at_idx": { + "name": "user_table_def_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_workspace_archived_partial_idx": { + "name": "user_table_def_workspace_archived_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"user_table_definitions\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_table_definitions_workspace_id_workspace_id_fk": { + "name": "user_table_definitions_workspace_id_workspace_id_fk", + "tableFrom": "user_table_definitions", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_table_definitions_created_by_user_id_fk": { + "name": "user_table_definitions_created_by_user_id_fk", + "tableFrom": "user_table_definitions", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_table_rows": { + "name": "user_table_rows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "order_key": { + "name": "order_key", + "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()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "user_table_rows_table_id_idx": { + "name": "user_table_rows_table_id_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_rows_data_gin_idx": { + "name": "user_table_rows_data_gin_idx", + "columns": [ + { + "expression": "data", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "user_table_rows_workspace_table_idx": { + "name": "user_table_rows_workspace_table_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_rows_table_position_idx": { + "name": "user_table_rows_table_position_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "position", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_rows_table_order_key_idx": { + "name": "user_table_rows_table_order_key_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "order_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_table_rows_table_id_user_table_definitions_id_fk": { + "name": "user_table_rows_table_id_user_table_definitions_id_fk", + "tableFrom": "user_table_rows", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_table_rows_workspace_id_workspace_id_fk": { + "name": "user_table_rows_workspace_id_workspace_id_fk", + "tableFrom": "user_table_rows", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_table_rows_created_by_user_id_fk": { + "name": "user_table_rows_created_by_user_id_fk", + "tableFrom": "user_table_rows", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "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 + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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 + }, + "archived_at": { + "name": "archived_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": { + "path_deployment_unique": { + "name": "path_deployment_unique", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"webhook\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_workflow_deployment_idx": { + "name": "webhook_workflow_deployment_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_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": {} + }, + "webhook_archived_at_partial_idx": { + "name": "webhook_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"webhook\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_webhook_on_provider_is_active_workflow_id_deploym_bdeed5468": { + "name": "idx_webhook_on_provider_is_active_workflow_id_deploym_bdeed5468", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_webhook_on_workflow_id_block_id_updated_at_desc": { + "name": "idx_webhook_on_workflow_id_block_id_updated_at_desc", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": false, + "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_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "webhook_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "webhook", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_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 + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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 + }, + "is_public_api": { + "name": "is_public_api", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "locked": { + "name": "locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": 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": "'{}'" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "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": {} + }, + "workflow_workspace_folder_name_active_unique": { + "name": "workflow_workspace_folder_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"folder_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_sort_idx": { + "name": "workflow_folder_sort_idx", + "columns": [ + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_archived_at_idx": { + "name": "workflow_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_workspace_archived_partial_idx": { + "name": "workflow_workspace_archived_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow\".\"archived_at\" IS NOT NULL", + "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 + }, + "locked": { + "name": "locked", + "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 + }, + "description": { + "name": "description", + "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": false + }, + "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 + }, + "cost_total": { + "name": "cost_total", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "models_used": { + "name": "models_used", + "type": "text[]", + "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": {} + }, + "workflow_execution_logs_workspace_cost_total_idx": { + "name": "workflow_execution_logs_workspace_cost_total_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost_total", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_models_used_idx": { + "name": "workflow_execution_logs_models_used_idx", + "columns": [ + { + "expression": "models_used", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "workflow_execution_logs_workspace_ended_at_id_idx": { + "name": "workflow_execution_logs_workspace_ended_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"ended_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_running_started_at_idx": { + "name": "workflow_execution_logs_running_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "status = 'running'", + "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": "set null", + "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": false + }, + "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": "set null", + "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 + }, + "locked": { + "name": "locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "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()" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "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": {} + }, + "workflow_folder_archived_at_idx": { + "name": "workflow_folder_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_workspace_archived_partial_idx": { + "name": "workflow_folder_workspace_archived_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_folder\".\"archived_at\" IS NOT NULL", + "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 + }, + "is_public": { + "name": "is_public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": 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": { + "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": {} + }, + "workflow_mcp_server_deleted_at_idx": { + "name": "workflow_mcp_server_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_server_workspace_deleted_partial_idx": { + "name": "workflow_mcp_server_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_mcp_server\".\"deleted_at\" IS NOT NULL", + "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": "'{}'" + }, + "archived_at": { + "name": "archived_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_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, + "where": "\"workflow_mcp_tool\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_archived_at_partial_idx": { + "name": "workflow_mcp_tool_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_mcp_tool\".\"archived_at\" IS NOT NULL", + "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": false + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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 + }, + "infra_retry_count": { + "name": "infra_retry_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 + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'workflow'" + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lifecycle": { + "name": "lifecycle", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'persistent'" + }, + "success_condition": { + "name": "success_condition", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "max_runs": { + "name": "max_runs", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "run_count": { + "name": "run_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "source_chat_id": { + "name": "source_chat_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_task_name": { + "name": "source_task_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_user_id": { + "name": "source_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_workspace_id": { + "name": "source_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "job_history": { + "name": "job_history", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_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_deployment_unique": { + "name": "workflow_schedule_workflow_block_deployment_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow_schedule\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_workflow_deployment_idx": { + "name": "workflow_schedule_workflow_deployment_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_archived_at_partial_idx": { + "name": "workflow_schedule_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_schedule\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_workflow_schedule_on_source_workspace_id_source_t_c07f3bba6": { + "name": "idx_workflow_schedule_on_source_workspace_id_source_t_c07f3bba6", + "columns": [ + { + "expression": "source_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_due_workflow_idx": { + "name": "workflow_schedule_due_workflow_idx", + "columns": [ + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_schedule\".\"archived_at\" IS NULL AND \"workflow_schedule\".\"status\" NOT IN ('disabled', 'completed') AND (\"workflow_schedule\".\"source_type\" = 'workflow' OR \"workflow_schedule\".\"source_type\" IS NULL)", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_due_job_idx": { + "name": "workflow_schedule_due_job_idx", + "columns": [ + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_schedule\".\"archived_at\" IS NULL AND \"workflow_schedule\".\"status\" NOT IN ('disabled', 'completed') AND \"workflow_schedule\".\"source_type\" = 'job'", + "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_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "workflow_schedule_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_source_user_id_user_id_fk": { + "name": "workflow_schedule_source_user_id_user_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "user", + "columnsFrom": ["source_user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_source_workspace_id_workspace_id_fk": { + "name": "workflow_schedule_source_workspace_id_workspace_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workspace", + "columnsFrom": ["source_workspace_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 + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'#33C482'" + }, + "logo_url": { + "name": "logo_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_mode": { + "name": "workspace_mode", + "type": "workspace_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'grandfathered_shared'" + }, + "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 + }, + "inbox_enabled": { + "name": "inbox_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "inbox_address": { + "name": "inbox_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "inbox_provider_id": { + "name": "inbox_provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_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": { + "workspace_owner_id_idx": { + "name": "workspace_owner_id_idx", + "columns": [ + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_organization_id_idx": { + "name": "workspace_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_mode_idx": { + "name": "workspace_mode_idx", + "columns": [ + { + "expression": "workspace_mode", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "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_organization_id_organization_id_fk": { + "name": "workspace_organization_id_organization_id_fk", + "tableFrom": "workspace", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "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 + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "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": {} + }, + "workspace_file_deleted_at_idx": { + "name": "workspace_file_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_workspace_deleted_partial_idx": { + "name": "workspace_file_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_file\".\"deleted_at\" IS NOT NULL", + "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_file_folders": { + "name": "workspace_file_folders", + "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 + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "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": { + "workspace_file_folders_workspace_parent_idx": { + "name": "workspace_file_folders_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": {} + }, + "workspace_file_folders_parent_sort_idx": { + "name": "workspace_file_folders_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": {} + }, + "workspace_file_folders_deleted_at_idx": { + "name": "workspace_file_folders_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_folders_workspace_deleted_partial_idx": { + "name": "workspace_file_folders_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_file_folders\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_folders_workspace_parent_name_active_unique": { + "name": "workspace_file_folders_workspace_parent_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"parent_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_file_folders\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_file_folders_user_id_user_id_fk": { + "name": "workspace_file_folders_user_id_user_id_fk", + "tableFrom": "workspace_file_folders", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_folders_workspace_id_workspace_id_fk": { + "name": "workspace_file_folders_workspace_id_workspace_id_fk", + "tableFrom": "workspace_file_folders", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_folders_parent_id_workspace_file_folders_id_fk": { + "name": "workspace_file_folders_parent_id_workspace_file_folders_id_fk", + "tableFrom": "workspace_file_folders", + "tableTo": "workspace_file_folders", + "columnsFrom": ["parent_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "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 + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "context": { + "name": "context", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "original_name": { + "name": "original_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_files_key_active_unique": { + "name": "workspace_files_key_active_unique", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_files\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_folder_name_active_unique": { + "name": "workspace_files_workspace_folder_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"folder_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "original_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_files\".\"deleted_at\" IS NULL AND \"workspace_files\".\"context\" = 'workspace' AND \"workspace_files\".\"workspace_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_chat_display_name_unique": { + "name": "workspace_files_chat_display_name_unique", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "display_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_files\".\"context\" = 'mothership' AND \"workspace_files\".\"chat_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "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_folder_id_idx": { + "name": "workspace_files_folder_id_idx", + "columns": [ + { + "expression": "folder_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": {} + }, + "workspace_files_chat_id_idx": { + "name": "workspace_files_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_deleted_at_idx": { + "name": "workspace_files_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_deleted_partial_idx": { + "name": "workspace_files_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_files\".\"deleted_at\" IS NOT NULL", + "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" + }, + "workspace_files_folder_id_workspace_file_folders_id_fk": { + "name": "workspace_files_folder_id_workspace_file_folders_id_fk", + "tableFrom": "workspace_files", + "tableTo": "workspace_file_folders", + "columnsFrom": ["folder_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_files_chat_id_copilot_chats_id_fk": { + "name": "workspace_files_chat_id_copilot_chats_id_fk", + "tableFrom": "workspace_files", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "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.academy_cert_status": { + "name": "academy_cert_status", + "schema": "public", + "values": ["active", "revoked", "expired"] + }, + "public.billing_blocked_reason": { + "name": "billing_blocked_reason", + "schema": "public", + "values": ["payment_failed", "dispute"] + }, + "public.billing_entity_type": { + "name": "billing_entity_type", + "schema": "public", + "values": ["user", "organization"] + }, + "public.chat_type": { + "name": "chat_type", + "schema": "public", + "values": ["mothership", "copilot"] + }, + "public.copilot_async_tool_status": { + "name": "copilot_async_tool_status", + "schema": "public", + "values": ["pending", "running", "completed", "failed", "cancelled", "delivered"] + }, + "public.copilot_run_status": { + "name": "copilot_run_status", + "schema": "public", + "values": ["active", "paused_waiting_for_tool", "resuming", "complete", "error", "cancelled"] + }, + "public.credential_member_role": { + "name": "credential_member_role", + "schema": "public", + "values": ["admin", "member"] + }, + "public.credential_member_status": { + "name": "credential_member_status", + "schema": "public", + "values": ["active", "pending", "revoked"] + }, + "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.credential_type": { + "name": "credential_type", + "schema": "public", + "values": ["oauth", "env_workspace", "env_personal", "service_account"] + }, + "public.data_drain_cadence": { + "name": "data_drain_cadence", + "schema": "public", + "values": ["hourly", "daily"] + }, + "public.data_drain_destination": { + "name": "data_drain_destination", + "schema": "public", + "values": ["s3", "gcs", "azure_blob", "datadog", "bigquery", "snowflake", "webhook"] + }, + "public.data_drain_run_status": { + "name": "data_drain_run_status", + "schema": "public", + "values": ["running", "success", "failed"] + }, + "public.data_drain_run_trigger": { + "name": "data_drain_run_trigger", + "schema": "public", + "values": ["cron", "manual"] + }, + "public.data_drain_source": { + "name": "data_drain_source", + "schema": "public", + "values": ["workflow_logs", "job_logs", "audit_logs", "copilot_chats", "copilot_runs"] + }, + "public.execution_large_value_reference_source": { + "name": "execution_large_value_reference_source", + "schema": "public", + "values": ["execution_log", "paused_snapshot"] + }, + "public.invitation_kind": { + "name": "invitation_kind", + "schema": "public", + "values": ["organization", "workspace"] + }, + "public.invitation_membership_intent": { + "name": "invitation_membership_intent", + "schema": "public", + "values": ["internal", "external"] + }, + "public.invitation_status": { + "name": "invitation_status", + "schema": "public", + "values": ["pending", "accepted", "rejected", "cancelled", "expired"] + }, + "public.permission_type": { + "name": "permission_type", + "schema": "public", + "values": ["admin", "write", "read"] + }, + "public.usage_log_category": { + "name": "usage_log_category", + "schema": "public", + "values": ["model", "fixed", "tool"] + }, + "public.usage_log_source": { + "name": "usage_log_source", + "schema": "public", + "values": [ + "workflow", + "wand", + "copilot", + "workspace-chat", + "mcp_copilot", + "mothership_block", + "knowledge-base", + "voice-input", + "enrichment" + ] + }, + "public.workspace_mode": { + "name": "workspace_mode", + "schema": "public", + "values": ["personal", "organization", "grandfathered_shared"] + } + }, + "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 55ca91ceaf0..3743536a186 100644 --- a/packages/db/migrations/meta/_journal.json +++ b/packages/db/migrations/meta/_journal.json @@ -1611,6 +1611,13 @@ "when": 1781027249389, "tag": "0230_thick_stranger", "breakpoints": true + }, + { + "idx": 231, + "version": "7", + "when": 1781053629977, + "tag": "0231_sim_trigger_workspace_events", + "breakpoints": true } ] } diff --git a/packages/db/schema.ts b/packages/db/schema.ts index a24c15d8c79..a9f4a447c84 100644 --- a/packages/db/schema.ts +++ b/packages/db/schema.ts @@ -767,91 +767,27 @@ export const webhook = pgTable( } ) -export const notificationTypeEnum = pgEnum('notification_type', ['webhook', 'email', 'slack']) - -export const notificationDeliveryStatusEnum = pgEnum('notification_delivery_status', [ - 'pending', - 'in_progress', - 'success', - 'failed', -]) - -export const workspaceNotificationSubscription = pgTable( - 'workspace_notification_subscription', - { - id: text('id').primaryKey(), - workspaceId: text('workspace_id') - .notNull() - .references(() => workspace.id, { onDelete: 'cascade' }), - notificationType: notificationTypeEnum('notification_type').notNull(), - workflowIds: text('workflow_ids').array().notNull().default(sql`'{}'::text[]`), - allWorkflows: boolean('all_workflows').notNull().default(false), - levelFilter: text('level_filter') - .array() - .notNull() - .default(sql`ARRAY['info', 'error']::text[]`), - triggerFilter: text('trigger_filter') - .array() - .notNull() - .default(sql`ARRAY['api', 'webhook', 'schedule', 'manual', 'chat']::text[]`), - includeFinalOutput: boolean('include_final_output').notNull().default(false), - includeTraceSpans: boolean('include_trace_spans').notNull().default(false), - includeRateLimits: boolean('include_rate_limits').notNull().default(false), - includeUsageData: boolean('include_usage_data').notNull().default(false), - - // Channel-specific configuration - webhookConfig: jsonb('webhook_config'), - emailRecipients: text('email_recipients').array(), - slackConfig: jsonb('slack_config'), - - // Alert rule configuration (if null, sends on every execution) - alertConfig: jsonb('alert_config'), - lastAlertAt: timestamp('last_alert_at'), - - active: boolean('active').notNull().default(true), - createdBy: text('created_by') - .notNull() - .references(() => user.id, { onDelete: 'cascade' }), - createdAt: timestamp('created_at').notNull().defaultNow(), - updatedAt: timestamp('updated_at').notNull().defaultNow(), - }, - (table) => ({ - workspaceIdIdx: index('workspace_notification_workspace_id_idx').on(table.workspaceId), - activeIdx: index('workspace_notification_active_idx').on(table.active), - typeIdx: index('workspace_notification_type_idx').on(table.notificationType), - }) -) - -export const workspaceNotificationDelivery = pgTable( - 'workspace_notification_delivery', +/** + * Cooldown state for Sim workspace-event trigger subscriptions. + * + * Keyed by (workflowId, blockId, scopeKey) rather than the webhook row because + * webhook rows are recreated per deployment version — state stored there would + * reset on every redeploy. `scopeKey` is '' for subscription-level cooldowns + * and the source workflow ID for per-source-workflow rules (no_activity). + */ +export const simTriggerState = pgTable( + 'sim_trigger_state', { - id: text('id').primaryKey(), - subscriptionId: text('subscription_id') - .notNull() - .references(() => workspaceNotificationSubscription.id, { onDelete: 'cascade' }), workflowId: text('workflow_id') .notNull() .references(() => workflow.id, { onDelete: 'cascade' }), - executionId: text('execution_id').notNull(), - status: notificationDeliveryStatusEnum('status').notNull().default('pending'), - attempts: integer('attempts').notNull().default(0), - lastAttemptAt: timestamp('last_attempt_at'), - nextAttemptAt: timestamp('next_attempt_at'), - responseStatus: integer('response_status'), - responseBody: text('response_body'), - errorMessage: text('error_message'), - createdAt: timestamp('created_at').notNull().defaultNow(), + blockId: text('block_id').notNull(), + scopeKey: text('scope_key').notNull().default(''), + lastFiredAt: timestamp('last_fired_at'), updatedAt: timestamp('updated_at').notNull().defaultNow(), }, (table) => ({ - subscriptionIdIdx: index('workspace_notification_delivery_subscription_id_idx').on( - table.subscriptionId - ), - executionIdIdx: index('workspace_notification_delivery_execution_id_idx').on(table.executionId), - statusIdx: index('workspace_notification_delivery_status_idx').on(table.status), - nextAttemptIdx: index('workspace_notification_delivery_next_attempt_idx').on( - table.nextAttemptAt - ), + pk: primaryKey({ columns: [table.workflowId, table.blockId, table.scopeKey] }), }) ) diff --git a/packages/testing/src/mocks/audit.mock.ts b/packages/testing/src/mocks/audit.mock.ts index 3cd9d624aff..30c2a71bb2d 100644 --- a/packages/testing/src/mocks/audit.mock.ts +++ b/packages/testing/src/mocks/audit.mock.ts @@ -101,9 +101,6 @@ export const auditMock = { MEMBER_INVITED: 'member.invited', MEMBER_REMOVED: 'member.removed', MEMBER_ROLE_CHANGED: 'member.role_changed', - NOTIFICATION_CREATED: 'notification.created', - NOTIFICATION_UPDATED: 'notification.updated', - NOTIFICATION_DELETED: 'notification.deleted', OAUTH_DISCONNECTED: 'oauth.disconnected', PASSWORD_RESET: 'password.reset', PASSWORD_RESET_REQUESTED: 'password.reset_requested', @@ -171,7 +168,6 @@ export const auditMock = { FOLDER: 'folder', KNOWLEDGE_BASE: 'knowledge_base', MCP_SERVER: 'mcp_server', - NOTIFICATION: 'notification', OAUTH: 'oauth', ORGANIZATION: 'organization', PASSWORD: 'password', diff --git a/packages/testing/src/mocks/database.mock.ts b/packages/testing/src/mocks/database.mock.ts index f4de1f54b4e..f055746753e 100644 --- a/packages/testing/src/mocks/database.mock.ts +++ b/packages/testing/src/mocks/database.mock.ts @@ -42,6 +42,10 @@ export function createMockSqlOperators() { lt: vi.fn((a, b) => ({ type: 'lt', left: a, right: b })), lte: vi.fn((a, b) => ({ type: 'lte', left: a, right: b })), count: vi.fn((column) => ({ type: 'count', column })), + avg: vi.fn((column) => ({ type: 'avg', column })), + sum: vi.fn((column) => ({ type: 'sum', column })), + min: vi.fn((column) => ({ type: 'min', column })), + max: vi.fn((column) => ({ type: 'max', column })), and: vi.fn((...conditions) => ({ type: 'and', conditions })), or: vi.fn((...conditions) => ({ type: 'or', conditions })), not: vi.fn((condition) => ({ type: 'not', condition })), diff --git a/packages/testing/src/mocks/schema.mock.ts b/packages/testing/src/mocks/schema.mock.ts index 5836a652851..ec12dc2d26f 100644 --- a/packages/testing/src/mocks/schema.mock.ts +++ b/packages/testing/src/mocks/schema.mock.ts @@ -307,43 +307,11 @@ export const schemaMock = { createdAt: 'createdAt', updatedAt: 'updatedAt', }, - notificationTypeEnum: 'notificationTypeEnum', - notificationDeliveryStatusEnum: 'notificationDeliveryStatusEnum', - workspaceNotificationSubscription: { - id: 'id', - workspaceId: 'workspaceId', - notificationType: 'notificationType', - workflowIds: 'workflowIds', - allWorkflows: 'allWorkflows', - levelFilter: 'levelFilter', - triggerFilter: 'triggerFilter', - includeFinalOutput: 'includeFinalOutput', - includeTraceSpans: 'includeTraceSpans', - includeRateLimits: 'includeRateLimits', - includeUsageData: 'includeUsageData', - webhookConfig: 'webhookConfig', - emailRecipients: 'emailRecipients', - slackConfig: 'slackConfig', - alertConfig: 'alertConfig', - lastAlertAt: 'lastAlertAt', - active: 'active', - createdBy: 'createdBy', - createdAt: 'createdAt', - updatedAt: 'updatedAt', - }, - workspaceNotificationDelivery: { - id: 'id', - subscriptionId: 'subscriptionId', + simTriggerState: { workflowId: 'workflowId', - executionId: 'executionId', - status: 'status', - attempts: 'attempts', - lastAttemptAt: 'lastAttemptAt', - nextAttemptAt: 'nextAttemptAt', - responseStatus: 'responseStatus', - responseBody: 'responseBody', - errorMessage: 'errorMessage', - createdAt: 'createdAt', + blockId: 'blockId', + scopeKey: 'scopeKey', + lastFiredAt: 'lastFiredAt', updatedAt: 'updatedAt', }, apiKey: { diff --git a/scripts/generate-docs.ts b/scripts/generate-docs.ts index 60a40994797..aad84b8f8db 100755 --- a/scripts/generate-docs.ts +++ b/scripts/generate-docs.ts @@ -33,7 +33,7 @@ const TRIGGERS_PATH = path.join(rootDir, 'apps/sim/triggers') const TRIGGER_DOCS_OUTPUT_PATH = path.join(rootDir, 'apps/docs/content/docs/en/triggers') /** Trigger doc pages that are hand-written and must never be overwritten. */ -const HANDWRITTEN_TRIGGER_DOCS = new Set(['index', 'start', 'schedule', 'webhook', 'rss']) +const HANDWRITTEN_TRIGGER_DOCS = new Set(['index', 'start', 'schedule', 'webhook', 'rss', 'sim']) /** Providers whose docs are already covered by hand-written pages. */ const SKIP_TRIGGER_PROVIDERS = new Set(['generic', 'rss']) @@ -1041,7 +1041,7 @@ function isIntegrationBlock(config: { category?: string; hideFromToolbar?: boole * generators, vision, and STT/TTS — is excluded from the integrations icon * map, matching {@link isIntegrationBlock}. */ -const ICON_MAP_BLOCK_CATEGORY_ALLOWLIST = new Set(['memory', 'knowledge']) +const ICON_MAP_BLOCK_CATEGORY_ALLOWLIST = new Set(['memory', 'knowledge', 'enrichment']) /** * Block types that never belong in the integrations icon map regardless of