diff --git a/apps/docs/content/docs/tools/airtable.mdx b/apps/docs/content/docs/tools/airtable.mdx index a078806f03..500bfab8ef 100644 --- a/apps/docs/content/docs/tools/airtable.mdx +++ b/apps/docs/content/docs/tools/airtable.mdx @@ -71,19 +71,16 @@ Read records from an Airtable table | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `accessToken` | string | Yes | OAuth access token | | `baseId` | string | Yes | ID of the Airtable base | | `tableId` | string | Yes | ID of the table | | `maxRecords` | number | No | Maximum number of records to return | -| `filterFormula` | string | No | Formula to filter records \(e.g., | +| `filterFormula` | string | No | Formula to filter records \(e.g., "\(\{Field Name\} = \'Value\'\)"\) | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `records` | json | Retrieved record data | -| `record` | json | Single record data | -| `metadata` | json | Operation metadata | +| `records` | json | Array of retrieved Airtable records | ### `airtable_get_record` @@ -93,7 +90,6 @@ Retrieve a single record from an Airtable table by its ID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `accessToken` | string | Yes | OAuth access token | | `baseId` | string | Yes | ID of the Airtable base | | `tableId` | string | Yes | ID or name of the table | | `recordId` | string | Yes | ID of the record to retrieve | @@ -102,9 +98,8 @@ Retrieve a single record from an Airtable table by its ID | Parameter | Type | Description | | --------- | ---- | ----------- | -| `records` | json | Retrieved record data | -| `record` | json | Single record data | -| `metadata` | json | Operation metadata | +| `record` | json | Retrieved Airtable record with id, createdTime, and fields | +| `metadata` | json | Operation metadata including record count | ### `airtable_create_records` @@ -114,17 +109,16 @@ Write new records to an Airtable table | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `accessToken` | string | Yes | OAuth access token | | `baseId` | string | Yes | ID of the Airtable base | | `tableId` | string | Yes | ID or name of the table | +| `records` | json | Yes | Array of records to create, each with a `fields` object | +| `fields` | string | No | No description | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `records` | json | Retrieved record data | -| `record` | json | Single record data | -| `metadata` | json | Operation metadata | +| `records` | json | Array of created Airtable records | ### `airtable_update_record` @@ -134,7 +128,6 @@ Update an existing record in an Airtable table by ID | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `accessToken` | string | Yes | OAuth access token | | `baseId` | string | Yes | ID of the Airtable base | | `tableId` | string | Yes | ID or name of the table | | `recordId` | string | Yes | ID of the record to update | @@ -144,9 +137,8 @@ Update an existing record in an Airtable table by ID | Parameter | Type | Description | | --------- | ---- | ----------- | -| `records` | json | Retrieved record data | -| `record` | json | Single record data | -| `metadata` | json | Operation metadata | +| `record` | json | Updated Airtable record with id, createdTime, and fields | +| `metadata` | json | Operation metadata including record count and updated field names | ### `airtable_update_multiple_records` @@ -156,17 +148,17 @@ Update multiple existing records in an Airtable table | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `accessToken` | string | Yes | OAuth access token | | `baseId` | string | Yes | ID of the Airtable base | | `tableId` | string | Yes | ID or name of the table | +| `records` | json | Yes | Array of records to update, each with an `id` and a `fields` object | +| `fields` | string | No | No description | +| `fields` | string | No | No description | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `records` | json | Retrieved record data | -| `record` | json | Single record data | -| `metadata` | json | Operation metadata | +| `records` | json | Array of updated Airtable records | diff --git a/apps/docs/content/docs/tools/arxiv.mdx b/apps/docs/content/docs/tools/arxiv.mdx index b18140b41f..95d7bbd333 100644 --- a/apps/docs/content/docs/tools/arxiv.mdx +++ b/apps/docs/content/docs/tools/arxiv.mdx @@ -71,10 +71,7 @@ Search for academic papers on ArXiv by keywords, authors, titles, or other field | Parameter | Type | Description | | --------- | ---- | ----------- | -| `papers` | json | Found papers data | -| `totalResults` | number | Total results count | -| `paper` | json | Paper details | -| `authorPapers` | json | Author papers list | +| `papers` | json | Array of papers matching the search query | ### `arxiv_get_paper` @@ -84,16 +81,13 @@ Get detailed information about a specific ArXiv paper by its ID. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `paperId` | string | Yes | ArXiv paper ID \(e.g., | +| `paperId` | string | Yes | ArXiv paper ID \(e.g., "1706.03762"\) | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `papers` | json | Found papers data | -| `totalResults` | number | Total results count | -| `paper` | json | Paper details | -| `authorPapers` | json | Author papers list | +| `paper` | json | Detailed information about the requested ArXiv paper | ### `arxiv_get_author_papers` @@ -110,10 +104,7 @@ Search for papers by a specific author on ArXiv. | Parameter | Type | Description | | --------- | ---- | ----------- | -| `papers` | json | Found papers data | -| `totalResults` | number | Total results count | -| `paper` | json | Paper details | -| `authorPapers` | json | Author papers list | +| `authorPapers` | json | Array of papers authored by the specified author | diff --git a/apps/docs/content/docs/tools/browser_use.mdx b/apps/docs/content/docs/tools/browser_use.mdx index 2063625a6e..7ca9783a8a 100644 --- a/apps/docs/content/docs/tools/browser_use.mdx +++ b/apps/docs/content/docs/tools/browser_use.mdx @@ -73,6 +73,7 @@ Runs a browser automation task using BrowserUse | --------- | ---- | -------- | ----------- | | `task` | string | Yes | What should the browser agent do | | `variables` | json | No | Optional variables to use as secrets \(format: \{key: value\}\) | +| `format` | string | No | No description | | `save_browser_data` | boolean | No | Whether to save browser data | | `model` | string | No | LLM model to use \(default: gpt-4o\) | | `apiKey` | string | Yes | API key for BrowserUse API | @@ -81,10 +82,9 @@ Runs a browser automation task using BrowserUse | Parameter | Type | Description | | --------- | ---- | ----------- | -| `id` | string | Task execution identifier | -| `success` | boolean | Task completion status | -| `output` | any | Task output data | -| `steps` | json | Execution steps taken | +| `success` | boolean | Operation success status | +| `output` | json | Browser automation task results including task ID, success status, output data, and execution steps | +| `error` | string | Error message if the operation failed | diff --git a/apps/docs/content/docs/tools/clay.mdx b/apps/docs/content/docs/tools/clay.mdx index 6320eabb0a..364fc9f462 100644 --- a/apps/docs/content/docs/tools/clay.mdx +++ b/apps/docs/content/docs/tools/clay.mdx @@ -220,7 +220,8 @@ Populate Clay with data from a JSON file. Enables direct communication and notif | Parameter | Type | Description | | --------- | ---- | ----------- | -| `data` | any | Response data | +| `success` | boolean | Operation success status | +| `output` | json | Clay populate operation results including response data from Clay webhook | diff --git a/apps/docs/content/docs/tools/confluence.mdx b/apps/docs/content/docs/tools/confluence.mdx index 311f3d7e24..981f39cf91 100644 --- a/apps/docs/content/docs/tools/confluence.mdx +++ b/apps/docs/content/docs/tools/confluence.mdx @@ -57,7 +57,6 @@ Retrieve content from Confluence pages using the Confluence API. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `accessToken` | string | Yes | OAuth access token for Confluence | | `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) | | `pageId` | string | Yes | Confluence page ID to retrieve | | `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. | @@ -66,11 +65,10 @@ Retrieve content from Confluence pages using the Confluence API. | Parameter | Type | Description | | --------- | ---- | ----------- | -| `ts` | string | Timestamp | -| `pageId` | string | Page identifier | -| `content` | string | Page content | +| `ts` | string | Timestamp of retrieval | +| `pageId` | string | Confluence page ID | +| `content` | string | Page content with HTML tags stripped | | `title` | string | Page title | -| `success` | boolean | Operation success status | ### `confluence_update` @@ -80,7 +78,6 @@ Update a Confluence page using the Confluence API. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `accessToken` | string | Yes | OAuth access token for Confluence | | `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) | | `pageId` | string | Yes | Confluence page ID to update | | `title` | string | No | New title for the page | @@ -92,11 +89,10 @@ Update a Confluence page using the Confluence API. | Parameter | Type | Description | | --------- | ---- | ----------- | -| `ts` | string | Timestamp | -| `pageId` | string | Page identifier | -| `content` | string | Page content | -| `title` | string | Page title | -| `success` | boolean | Operation success status | +| `ts` | string | Timestamp of update | +| `pageId` | string | Confluence page ID | +| `title` | string | Updated page title | +| `success` | boolean | Update operation success status | diff --git a/apps/docs/content/docs/tools/discord.mdx b/apps/docs/content/docs/tools/discord.mdx index 674e834e3e..a96c49964a 100644 --- a/apps/docs/content/docs/tools/discord.mdx +++ b/apps/docs/content/docs/tools/discord.mdx @@ -80,8 +80,8 @@ Send a message to a Discord channel | Parameter | Type | Description | | --------- | ---- | ----------- | -| `message` | string | Message content | -| `data` | any | Response data | +| `message` | string | Success or error message | +| `data` | object | Discord message data | ### `discord_get_messages` @@ -99,8 +99,8 @@ Retrieve messages from a Discord channel | Parameter | Type | Description | | --------- | ---- | ----------- | -| `message` | string | Message content | -| `data` | any | Response data | +| `message` | string | Success or error message | +| `messages` | array | Array of Discord messages with full metadata | ### `discord_get_server` @@ -117,8 +117,8 @@ Retrieve information about a Discord server (guild) | Parameter | Type | Description | | --------- | ---- | ----------- | -| `message` | string | Message content | -| `data` | any | Response data | +| `message` | string | Success or error message | +| `data` | object | Discord server \(guild\) information | ### `discord_get_user` @@ -135,8 +135,8 @@ Retrieve information about a Discord user | Parameter | Type | Description | | --------- | ---- | ----------- | -| `message` | string | Message content | -| `data` | any | Response data | +| `message` | string | Success or error message | +| `data` | object | Discord user information | diff --git a/apps/docs/content/docs/tools/exa.mdx b/apps/docs/content/docs/tools/exa.mdx index 34e860ff2c..baaaace971 100644 --- a/apps/docs/content/docs/tools/exa.mdx +++ b/apps/docs/content/docs/tools/exa.mdx @@ -68,11 +68,7 @@ Search the web using Exa AI. Returns relevant search results with titles, URLs, | Parameter | Type | Description | | --------- | ---- | ----------- | -| `results` | json | Search results | -| `similarLinks` | json | Similar links found | -| `answer` | string | Generated answer | -| `citations` | json | Answer citations | -| `research` | json | Research findings | +| `results` | array | Search results with titles, URLs, and text snippets | ### `exa_get_contents` @@ -91,11 +87,7 @@ Retrieve the contents of webpages using Exa AI. Returns the title, text content, | Parameter | Type | Description | | --------- | ---- | ----------- | -| `results` | json | Search results | -| `similarLinks` | json | Similar links found | -| `answer` | string | Generated answer | -| `citations` | json | Answer citations | -| `research` | json | Research findings | +| `results` | array | Retrieved content from URLs with title, text, and summaries | ### `exa_find_similar_links` @@ -114,11 +106,7 @@ Find webpages similar to a given URL using Exa AI. Returns a list of similar lin | Parameter | Type | Description | | --------- | ---- | ----------- | -| `results` | json | Search results | -| `similarLinks` | json | Similar links found | -| `answer` | string | Generated answer | -| `citations` | json | Answer citations | -| `research` | json | Research findings | +| `similarLinks` | array | Similar links found with titles, URLs, and text snippets | ### `exa_answer` @@ -136,11 +124,8 @@ Get an AI-generated answer to a question with citations from the web using Exa A | Parameter | Type | Description | | --------- | ---- | ----------- | -| `results` | json | Search results | -| `similarLinks` | json | Similar links found | -| `answer` | string | Generated answer | -| `citations` | json | Answer citations | -| `research` | json | Research findings | +| `answer` | string | AI-generated answer to the question | +| `citations` | array | Sources and citations for the answer | ### `exa_research` @@ -158,11 +143,7 @@ Perform comprehensive research using AI to generate detailed reports with citati | Parameter | Type | Description | | --------- | ---- | ----------- | -| `results` | json | Search results | -| `similarLinks` | json | Similar links found | -| `answer` | string | Generated answer | -| `citations` | json | Answer citations | -| `research` | json | Research findings | +| `research` | array | Comprehensive research findings with citations and summaries | diff --git a/apps/docs/content/docs/tools/file.mdx b/apps/docs/content/docs/tools/file.mdx index 88a3fecf8e..a7c326908e 100644 --- a/apps/docs/content/docs/tools/file.mdx +++ b/apps/docs/content/docs/tools/file.mdx @@ -71,8 +71,8 @@ Parse one or more uploaded files or files from URLs (text, PDF, CSV, images, etc | Parameter | Type | Description | | --------- | ---- | ----------- | -| `files` | json | Parsed file data | -| `combinedContent` | string | Combined file content | +| `files` | json | Array of parsed file objects with content, metadata, and file properties | +| `combinedContent` | string | All file contents merged into a single text string | diff --git a/apps/docs/content/docs/tools/firecrawl.mdx b/apps/docs/content/docs/tools/firecrawl.mdx index ba28ff956f..c478a258eb 100644 --- a/apps/docs/content/docs/tools/firecrawl.mdx +++ b/apps/docs/content/docs/tools/firecrawl.mdx @@ -81,14 +81,9 @@ Extract structured content from web pages with comprehensive metadata support. C | Parameter | Type | Description | | --------- | ---- | ----------- | -| `markdown` | string | Page content markdown | -| `html` | any | Raw HTML content | -| `metadata` | json | Page metadata | -| `data` | json | Search results data | -| `warning` | any | Warning messages | -| `pages` | json | Crawled pages data | -| `total` | number | Total pages found | -| `creditsUsed` | number | Credits consumed | +| `markdown` | string | Page content in markdown format | +| `html` | string | Raw HTML content of the page | +| `metadata` | object | Page metadata including SEO and Open Graph information | ### `firecrawl_search` @@ -105,14 +100,7 @@ Search for information on the web using Firecrawl | Parameter | Type | Description | | --------- | ---- | ----------- | -| `markdown` | string | Page content markdown | -| `html` | any | Raw HTML content | -| `metadata` | json | Page metadata | -| `data` | json | Search results data | -| `warning` | any | Warning messages | -| `pages` | json | Crawled pages data | -| `total` | number | Total pages found | -| `creditsUsed` | number | Credits consumed | +| `data` | array | Search results data | ### `firecrawl_crawl` @@ -131,14 +119,7 @@ Crawl entire websites and extract structured content from all accessible pages | Parameter | Type | Description | | --------- | ---- | ----------- | -| `markdown` | string | Page content markdown | -| `html` | any | Raw HTML content | -| `metadata` | json | Page metadata | -| `data` | json | Search results data | -| `warning` | any | Warning messages | -| `pages` | json | Crawled pages data | -| `total` | number | Total pages found | -| `creditsUsed` | number | Credits consumed | +| `pages` | array | Array of crawled pages with their content and metadata | diff --git a/apps/docs/content/docs/tools/generic_webhook.mdx b/apps/docs/content/docs/tools/generic_webhook.mdx new file mode 100644 index 0000000000..048916b7a5 --- /dev/null +++ b/apps/docs/content/docs/tools/generic_webhook.mdx @@ -0,0 +1,32 @@ +--- +title: Webhook +description: Receive webhooks from any service +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + + + `} +/> + + + + + +## Notes + +- Category: `triggers` +- Type: `generic_webhook` diff --git a/apps/docs/content/docs/tools/github.mdx b/apps/docs/content/docs/tools/github.mdx index b9b48839af..3169cf4f72 100644 --- a/apps/docs/content/docs/tools/github.mdx +++ b/apps/docs/content/docs/tools/github.mdx @@ -1,6 +1,6 @@ --- title: GitHub -description: Interact with GitHub +description: Interact with GitHub or trigger workflows from GitHub events --- import { BlockInfoCard } from "@/components/ui/block-info-card" @@ -35,7 +35,7 @@ In Sim, the GitHub integration enables your agents to interact directly with Git ## Usage Instructions -Access GitHub repositories, pull requests, and comments through the GitHub API. Automate code reviews, PR management, and repository interactions within your workflow. +Access GitHub repositories, pull requests, and comments through the GitHub API. Automate code reviews, PR management, and repository interactions within your workflow. Trigger workflows from GitHub events like push, pull requests, and issues. @@ -58,8 +58,8 @@ Fetch PR details including diff and files changed | Parameter | Type | Description | | --------- | ---- | ----------- | -| `content` | string | Response content | -| `metadata` | json | Response metadata | +| `content` | string | Human-readable PR summary | +| `metadata` | object | Detailed PR metadata including file changes | ### `github_comment` @@ -85,8 +85,8 @@ Create comments on GitHub PRs | Parameter | Type | Description | | --------- | ---- | ----------- | -| `content` | string | Response content | -| `metadata` | json | Response metadata | +| `content` | string | Human-readable comment confirmation | +| `metadata` | object | Comment metadata | ### `github_repo_info` @@ -104,8 +104,8 @@ Retrieve comprehensive GitHub repository metadata including stars, forks, issues | Parameter | Type | Description | | --------- | ---- | ----------- | -| `content` | string | Response content | -| `metadata` | json | Response metadata | +| `content` | string | Human-readable repository summary | +| `metadata` | object | Repository metadata | ### `github_latest_commit` @@ -117,15 +117,15 @@ Retrieve the latest commit from a GitHub repository | --------- | ---- | -------- | ----------- | | `owner` | string | Yes | Repository owner \(user or organization\) | | `repo` | string | Yes | Repository name | -| `branch` | string | No | Branch name \(defaults to the repository | +| `branch` | string | No | Branch name \(defaults to the repository's default branch\) | | `apiKey` | string | Yes | GitHub API token | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `content` | string | Response content | -| `metadata` | json | Response metadata | +| `content` | string | Human-readable commit summary | +| `metadata` | object | Commit metadata | diff --git a/apps/docs/content/docs/tools/gmail.mdx b/apps/docs/content/docs/tools/gmail.mdx index a12d6cc67b..3da6f2aaa7 100644 --- a/apps/docs/content/docs/tools/gmail.mdx +++ b/apps/docs/content/docs/tools/gmail.mdx @@ -1,6 +1,6 @@ --- title: Gmail -description: Send Gmail +description: Send Gmail or trigger workflows from Gmail events --- import { BlockInfoCard } from "@/components/ui/block-info-card" @@ -51,7 +51,7 @@ In Sim, the Gmail integration enables your agents to send, read, and search emai ## Usage Instructions -Integrate Gmail functionality to send email messages within your workflow. Automate email communications and process email content using OAuth authentication. +Comprehensive Gmail integration with OAuth authentication. Send email messages, read email content, and trigger workflows from Gmail events like new emails and label changes. @@ -65,17 +65,18 @@ Send emails using Gmail | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `accessToken` | string | Yes | Access token for Gmail API | | `to` | string | Yes | Recipient email address | | `subject` | string | Yes | Email subject | | `body` | string | Yes | Email body content | +| `cc` | string | No | CC recipients \(comma-separated\) | +| `bcc` | string | No | BCC recipients \(comma-separated\) | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `content` | string | Response content | -| `metadata` | json | Email metadata | +| `content` | string | Success message | +| `metadata` | object | Email metadata | ### `gmail_draft` @@ -85,17 +86,18 @@ Draft emails using Gmail | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `accessToken` | string | Yes | Access token for Gmail API | | `to` | string | Yes | Recipient email address | | `subject` | string | Yes | Email subject | | `body` | string | Yes | Email body content | +| `cc` | string | No | CC recipients \(comma-separated\) | +| `bcc` | string | No | BCC recipients \(comma-separated\) | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `content` | string | Response content | -| `metadata` | json | Email metadata | +| `content` | string | Success message | +| `metadata` | object | Draft metadata | ### `gmail_read` @@ -105,18 +107,19 @@ Read emails from Gmail | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `accessToken` | string | Yes | Access token for Gmail API | | `messageId` | string | No | ID of the message to read | | `folder` | string | No | Folder/label to read emails from | | `unreadOnly` | boolean | No | Only retrieve unread messages | | `maxResults` | number | No | Maximum number of messages to retrieve \(default: 1, max: 10\) | +| `includeAttachments` | boolean | No | Download and include email attachments | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `content` | string | Response content | -| `metadata` | json | Email metadata | +| `content` | string | Text content of the email | +| `metadata` | json | Metadata of the email | +| `attachments` | file[] | Attachments of the email | ### `gmail_search` @@ -126,7 +129,6 @@ Search emails in Gmail | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `accessToken` | string | Yes | Access token for Gmail API | | `query` | string | Yes | Search query for emails | | `maxResults` | number | No | Maximum number of results to return | @@ -134,8 +136,8 @@ Search emails in Gmail | Parameter | Type | Description | | --------- | ---- | ----------- | -| `content` | string | Response content | -| `metadata` | json | Email metadata | +| `content` | string | Search results summary | +| `metadata` | object | Search metadata | diff --git a/apps/docs/content/docs/tools/google_calendar.mdx b/apps/docs/content/docs/tools/google_calendar.mdx index eb02695622..b51c247c31 100644 --- a/apps/docs/content/docs/tools/google_calendar.mdx +++ b/apps/docs/content/docs/tools/google_calendar.mdx @@ -104,7 +104,6 @@ Create a new event in Google Calendar | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `accessToken` | string | Yes | Access token for Google Calendar API | | `calendarId` | string | No | Calendar ID \(defaults to primary\) | | `summary` | string | Yes | Event title/summary | | `description` | string | No | Event description | @@ -119,8 +118,8 @@ Create a new event in Google Calendar | Parameter | Type | Description | | --------- | ---- | ----------- | -| `content` | string | Operation response content | -| `metadata` | json | Event metadata | +| `content` | string | Event creation confirmation message | +| `metadata` | json | Created event metadata including ID, status, and details | ### `google_calendar_list` @@ -130,7 +129,6 @@ List events from Google Calendar | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `accessToken` | string | Yes | Access token for Google Calendar API | | `calendarId` | string | No | Calendar ID \(defaults to primary\) | | `timeMin` | string | No | Lower bound for events \(RFC3339 timestamp, e.g., 2025-06-03T00:00:00Z\) | | `timeMax` | string | No | Upper bound for events \(RFC3339 timestamp, e.g., 2025-06-04T00:00:00Z\) | @@ -141,8 +139,8 @@ List events from Google Calendar | Parameter | Type | Description | | --------- | ---- | ----------- | -| `content` | string | Operation response content | -| `metadata` | json | Event metadata | +| `content` | string | Summary of found events count | +| `metadata` | json | List of events with pagination tokens and event details | ### `google_calendar_get` @@ -152,7 +150,6 @@ Get a specific event from Google Calendar | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `accessToken` | string | Yes | Access token for Google Calendar API | | `calendarId` | string | No | Calendar ID \(defaults to primary\) | | `eventId` | string | Yes | Event ID to retrieve | @@ -160,8 +157,8 @@ Get a specific event from Google Calendar | Parameter | Type | Description | | --------- | ---- | ----------- | -| `content` | string | Operation response content | -| `metadata` | json | Event metadata | +| `content` | string | Event retrieval confirmation message | +| `metadata` | json | Event details including ID, status, times, and attendees | ### `google_calendar_quick_add` @@ -171,9 +168,8 @@ Create events from natural language text | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `accessToken` | string | Yes | Access token for Google Calendar API | | `calendarId` | string | No | Calendar ID \(defaults to primary\) | -| `text` | string | Yes | Natural language text describing the event \(e.g., | +| `text` | string | Yes | Natural language text describing the event \(e.g., "Meeting with John tomorrow at 3pm"\) | | `attendees` | array | No | Array of attendee email addresses \(comma-separated string also accepted\) | | `sendUpdates` | string | No | How to send updates to attendees: all, externalOnly, or none | @@ -181,8 +177,8 @@ Create events from natural language text | Parameter | Type | Description | | --------- | ---- | ----------- | -| `content` | string | Operation response content | -| `metadata` | json | Event metadata | +| `content` | string | Event creation confirmation message from natural language | +| `metadata` | json | Created event metadata including parsed details | ### `google_calendar_invite` @@ -192,7 +188,6 @@ Invite attendees to an existing Google Calendar event | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `accessToken` | string | Yes | Access token for Google Calendar API | | `calendarId` | string | No | Calendar ID \(defaults to primary\) | | `eventId` | string | Yes | Event ID to invite attendees to | | `attendees` | array | Yes | Array of attendee email addresses to invite | @@ -203,8 +198,8 @@ Invite attendees to an existing Google Calendar event | Parameter | Type | Description | | --------- | ---- | ----------- | -| `content` | string | Operation response content | -| `metadata` | json | Event metadata | +| `content` | string | Attendee invitation confirmation message with email delivery status | +| `metadata` | json | Updated event metadata including attendee list and details | diff --git a/apps/docs/content/docs/tools/google_docs.mdx b/apps/docs/content/docs/tools/google_docs.mdx index cfac1a14d4..593d74fc28 100644 --- a/apps/docs/content/docs/tools/google_docs.mdx +++ b/apps/docs/content/docs/tools/google_docs.mdx @@ -95,16 +95,14 @@ Read content from a Google Docs document | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `accessToken` | string | Yes | The access token for the Google Docs API | | `documentId` | string | Yes | The ID of the document to read | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `content` | string | Document content | -| `metadata` | json | Document metadata | -| `updatedContent` | boolean | Content update status | +| `content` | string | Extracted document text content | +| `metadata` | json | Document metadata including ID, title, and URL | ### `google_docs_write` @@ -114,7 +112,6 @@ Write or update content in a Google Docs document | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `accessToken` | string | Yes | The access token for the Google Docs API | | `documentId` | string | Yes | The ID of the document to write to | | `content` | string | Yes | The content to write to the document | @@ -122,9 +119,8 @@ Write or update content in a Google Docs document | Parameter | Type | Description | | --------- | ---- | ----------- | -| `content` | string | Document content | -| `metadata` | json | Document metadata | -| `updatedContent` | boolean | Content update status | +| `updatedContent` | boolean | Indicates if document content was updated successfully | +| `metadata` | json | Updated document metadata including ID, title, and URL | ### `google_docs_create` @@ -134,7 +130,6 @@ Create a new Google Docs document | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `accessToken` | string | Yes | The access token for the Google Docs API | | `title` | string | Yes | The title of the document to create | | `content` | string | No | The content of the document to create | | `folderSelector` | string | No | Select the folder to create the document in | @@ -144,9 +139,7 @@ Create a new Google Docs document | Parameter | Type | Description | | --------- | ---- | ----------- | -| `content` | string | Document content | -| `metadata` | json | Document metadata | -| `updatedContent` | boolean | Content update status | +| `metadata` | json | Created document metadata including ID, title, and URL | diff --git a/apps/docs/content/docs/tools/google_drive.mdx b/apps/docs/content/docs/tools/google_drive.mdx index d729ff6d52..f6a2e1ec75 100644 --- a/apps/docs/content/docs/tools/google_drive.mdx +++ b/apps/docs/content/docs/tools/google_drive.mdx @@ -87,7 +87,6 @@ Upload a file to Google Drive | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `accessToken` | string | Yes | The access token for the Google Drive API | | `fileName` | string | Yes | The name of the file to upload | | `content` | string | Yes | The content of the file to upload | | `mimeType` | string | No | The MIME type of the file to upload | @@ -98,8 +97,7 @@ Upload a file to Google Drive | Parameter | Type | Description | | --------- | ---- | ----------- | -| `file` | json | File data | -| `files` | json | Files list | +| `file` | json | Uploaded file metadata including ID, name, and links | ### `google_drive_create_folder` @@ -109,7 +107,6 @@ Create a new folder in Google Drive | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `accessToken` | string | Yes | The access token for the Google Drive API | | `fileName` | string | Yes | Name of the folder to create | | `folderSelector` | string | No | Select the parent folder to create the folder in | | `folderId` | string | No | ID of the parent folder \(internal use\) | @@ -118,8 +115,7 @@ Create a new folder in Google Drive | Parameter | Type | Description | | --------- | ---- | ----------- | -| `file` | json | File data | -| `files` | json | Files list | +| `file` | json | Created folder metadata including ID, name, and parent information | ### `google_drive_list` @@ -129,7 +125,6 @@ List files and folders in Google Drive | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `accessToken` | string | Yes | The access token for the Google Drive API | | `folderSelector` | string | No | Select the folder to list files from | | `folderId` | string | No | The ID of the folder to list files from \(internal use\) | | `query` | string | No | A query to filter the files | @@ -140,8 +135,7 @@ List files and folders in Google Drive | Parameter | Type | Description | | --------- | ---- | ----------- | -| `file` | json | File data | -| `files` | json | Files list | +| `files` | json | Array of file metadata objects from the specified folder | diff --git a/apps/docs/content/docs/tools/google_search.mdx b/apps/docs/content/docs/tools/google_search.mdx index b98863031e..805881f63f 100644 --- a/apps/docs/content/docs/tools/google_search.mdx +++ b/apps/docs/content/docs/tools/google_search.mdx @@ -81,8 +81,7 @@ Search the web with the Custom Search API | Parameter | Type | Description | | --------- | ---- | ----------- | -| `items` | json | Search result items | -| `searchInformation` | json | Search metadata | +| `items` | array | Array of search results from Google | diff --git a/apps/docs/content/docs/tools/google_sheets.mdx b/apps/docs/content/docs/tools/google_sheets.mdx index 00909a3608..9339cdaab8 100644 --- a/apps/docs/content/docs/tools/google_sheets.mdx +++ b/apps/docs/content/docs/tools/google_sheets.mdx @@ -110,7 +110,6 @@ Read data from a Google Sheets spreadsheet | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `accessToken` | string | Yes | The access token for the Google Sheets API | | `spreadsheetId` | string | Yes | The ID of the spreadsheet to read from | | `range` | string | No | The range of cells to read from | @@ -118,13 +117,8 @@ Read data from a Google Sheets spreadsheet | Parameter | Type | Description | | --------- | ---- | ----------- | -| `data` | json | Sheet data | -| `metadata` | json | Operation metadata | -| `updatedRange` | string | Updated range | -| `updatedRows` | number | Updated rows count | -| `updatedColumns` | number | Updated columns count | -| `updatedCells` | number | Updated cells count | -| `tableRange` | string | Table range | +| `data` | json | Sheet data including range and cell values | +| `metadata` | json | Spreadsheet metadata including ID and URL | ### `google_sheets_write` @@ -134,7 +128,6 @@ Write data to a Google Sheets spreadsheet | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `accessToken` | string | Yes | The access token for the Google Sheets API | | `spreadsheetId` | string | Yes | The ID of the spreadsheet to write to | | `range` | string | No | The range of cells to write to | | `values` | array | Yes | The data to write to the spreadsheet | @@ -145,13 +138,11 @@ Write data to a Google Sheets spreadsheet | Parameter | Type | Description | | --------- | ---- | ----------- | -| `data` | json | Sheet data | -| `metadata` | json | Operation metadata | -| `updatedRange` | string | Updated range | -| `updatedRows` | number | Updated rows count | -| `updatedColumns` | number | Updated columns count | -| `updatedCells` | number | Updated cells count | -| `tableRange` | string | Table range | +| `updatedRange` | string | Range of cells that were updated | +| `updatedRows` | number | Number of rows updated | +| `updatedColumns` | number | Number of columns updated | +| `updatedCells` | number | Number of cells updated | +| `metadata` | json | Spreadsheet metadata including ID and URL | ### `google_sheets_update` @@ -161,7 +152,6 @@ Update data in a Google Sheets spreadsheet | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `accessToken` | string | Yes | The access token for the Google Sheets API | | `spreadsheetId` | string | Yes | The ID of the spreadsheet to update | | `range` | string | No | The range of cells to update | | `values` | array | Yes | The data to update in the spreadsheet | @@ -172,13 +162,11 @@ Update data in a Google Sheets spreadsheet | Parameter | Type | Description | | --------- | ---- | ----------- | -| `data` | json | Sheet data | -| `metadata` | json | Operation metadata | -| `updatedRange` | string | Updated range | -| `updatedRows` | number | Updated rows count | -| `updatedColumns` | number | Updated columns count | -| `updatedCells` | number | Updated cells count | -| `tableRange` | string | Table range | +| `updatedRange` | string | Range of cells that were updated | +| `updatedRows` | number | Number of rows updated | +| `updatedColumns` | number | Number of columns updated | +| `updatedCells` | number | Number of cells updated | +| `metadata` | json | Spreadsheet metadata including ID and URL | ### `google_sheets_append` @@ -188,7 +176,6 @@ Append data to the end of a Google Sheets spreadsheet | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `accessToken` | string | Yes | The access token for the Google Sheets API | | `spreadsheetId` | string | Yes | The ID of the spreadsheet to append to | | `range` | string | No | The range of cells to append after | | `values` | array | Yes | The data to append to the spreadsheet | @@ -200,13 +187,12 @@ Append data to the end of a Google Sheets spreadsheet | Parameter | Type | Description | | --------- | ---- | ----------- | -| `data` | json | Sheet data | -| `metadata` | json | Operation metadata | -| `updatedRange` | string | Updated range | -| `updatedRows` | number | Updated rows count | -| `updatedColumns` | number | Updated columns count | -| `updatedCells` | number | Updated cells count | -| `tableRange` | string | Table range | +| `tableRange` | string | Range of the table where data was appended | +| `updatedRange` | string | Range of cells that were updated | +| `updatedRows` | number | Number of rows updated | +| `updatedColumns` | number | Number of columns updated | +| `updatedCells` | number | Number of cells updated | +| `metadata` | json | Spreadsheet metadata including ID and URL | diff --git a/apps/docs/content/docs/tools/huggingface.mdx b/apps/docs/content/docs/tools/huggingface.mdx index 6e0875a4ec..afba281339 100644 --- a/apps/docs/content/docs/tools/huggingface.mdx +++ b/apps/docs/content/docs/tools/huggingface.mdx @@ -92,9 +92,8 @@ Generate completions using Hugging Face Inference API | Parameter | Type | Description | | --------- | ---- | ----------- | -| `content` | string | Generated response | -| `model` | string | Model used | -| `usage` | json | Token usage stats | +| `success` | boolean | Operation success status | +| `output` | object | Chat completion results | diff --git a/apps/docs/content/docs/tools/hunter.mdx b/apps/docs/content/docs/tools/hunter.mdx index f7f2c17fa2..03e2309957 100644 --- a/apps/docs/content/docs/tools/hunter.mdx +++ b/apps/docs/content/docs/tools/hunter.mdx @@ -57,7 +57,7 @@ Returns companies matching a set of criteria using Hunter.io AI-powered search. | --------- | ---- | -------- | ----------- | | `query` | string | No | Natural language search query for companies | | `domain` | string | No | Company domain names to filter by | -| `headcount` | string | No | Company size filter \(e.g., | +| `headcount` | string | No | Company size filter \(e.g., "1-10", "11-50"\) | | `company_type` | string | No | Type of organization | | `technology` | string | No | Technology used by companies | | `apiKey` | string | Yes | Hunter.io API Key | @@ -66,15 +66,7 @@ Returns companies matching a set of criteria using Hunter.io AI-powered search. | Parameter | Type | Description | | --------- | ---- | ----------- | -| `results` | json | Search results | -| `emails` | json | Email addresses found | -| `email` | string | Found email address | -| `score` | number | Confidence score | -| `result` | string | Verification result | -| `status` | string | Status message | -| `total` | number | Total results count | -| `personal_emails` | number | Personal emails count | -| `generic_emails` | number | Generic emails count | +| `results` | array | Array of companies matching the search criteria, each containing domain, name, headcount, technologies, and email_count | ### `hunter_domain_search` @@ -96,15 +88,26 @@ Returns all the email addresses found using one given domain name, with sources. | Parameter | Type | Description | | --------- | ---- | ----------- | -| `results` | json | Search results | -| `emails` | json | Email addresses found | -| `email` | string | Found email address | -| `score` | number | Confidence score | -| `result` | string | Verification result | -| `status` | string | Status message | -| `total` | number | Total results count | -| `personal_emails` | number | Personal emails count | -| `generic_emails` | number | Generic emails count | +| `domain` | string | The searched domain name | +| `disposable` | boolean | Whether the domain accepts disposable email addresses | +| `webmail` | boolean | Whether the domain is a webmail provider | +| `accept_all` | boolean | Whether the domain accepts all email addresses | +| `pattern` | string | The email pattern used by the organization | +| `organization` | string | The organization name | +| `description` | string | Description of the organization | +| `industry` | string | Industry of the organization | +| `twitter` | string | Twitter profile of the organization | +| `facebook` | string | Facebook profile of the organization | +| `linkedin` | string | LinkedIn profile of the organization | +| `instagram` | string | Instagram profile of the organization | +| `youtube` | string | YouTube channel of the organization | +| `technologies` | array | Array of technologies used by the organization | +| `country` | string | Country where the organization is located | +| `state` | string | State where the organization is located | +| `city` | string | City where the organization is located | +| `postal_code` | string | Postal code of the organization | +| `street` | string | Street address of the organization | +| `emails` | array | Array of email addresses found for the domain, each containing value, type, confidence, sources, and person details | ### `hunter_email_finder` @@ -115,8 +118,8 @@ Finds the most likely email address for a person given their name and company do | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `domain` | string | Yes | Company domain name | -| `first_name` | string | Yes | Person | -| `last_name` | string | Yes | Person | +| `first_name` | string | Yes | Person's first name | +| `last_name` | string | Yes | Person's last name | | `company` | string | No | Company name | | `apiKey` | string | Yes | Hunter.io API Key | @@ -124,15 +127,10 @@ Finds the most likely email address for a person given their name and company do | Parameter | Type | Description | | --------- | ---- | ----------- | -| `results` | json | Search results | -| `emails` | json | Email addresses found | -| `email` | string | Found email address | -| `score` | number | Confidence score | -| `result` | string | Verification result | -| `status` | string | Status message | -| `total` | number | Total results count | -| `personal_emails` | number | Personal emails count | -| `generic_emails` | number | Generic emails count | +| `email` | string | The found email address | +| `score` | number | Confidence score for the found email address | +| `sources` | array | Array of sources where the email was found, each containing domain, uri, extracted_on, last_seen_on, and still_on_page | +| `verification` | object | Verification information containing date and status | ### `hunter_email_verifier` @@ -149,15 +147,20 @@ Verifies the deliverability of an email address and provides detailed verificati | Parameter | Type | Description | | --------- | ---- | ----------- | -| `results` | json | Search results | -| `emails` | json | Email addresses found | -| `email` | string | Found email address | -| `score` | number | Confidence score | -| `result` | string | Verification result | -| `status` | string | Status message | -| `total` | number | Total results count | -| `personal_emails` | number | Personal emails count | -| `generic_emails` | number | Generic emails count | +| `result` | string | Deliverability result: deliverable, undeliverable, or risky | +| `score` | number | Confidence score for the verification result | +| `email` | string | The verified email address | +| `regexp` | boolean | Whether the email follows a valid regex pattern | +| `gibberish` | boolean | Whether the email appears to be gibberish | +| `disposable` | boolean | Whether the email is from a disposable email provider | +| `webmail` | boolean | Whether the email is from a webmail provider | +| `mx_records` | boolean | Whether MX records exist for the domain | +| `smtp_server` | boolean | Whether the SMTP server is reachable | +| `smtp_check` | boolean | Whether the SMTP check was successful | +| `accept_all` | boolean | Whether the domain accepts all email addresses | +| `block` | boolean | Whether the email is blocked | +| `status` | string | Verification status: valid, invalid, accept_all, webmail, disposable, or unknown | +| `sources` | array | Array of sources where the email was found | ### `hunter_companies_find` @@ -174,15 +177,8 @@ Enriches company data using domain name. | Parameter | Type | Description | | --------- | ---- | ----------- | -| `results` | json | Search results | -| `emails` | json | Email addresses found | -| `email` | string | Found email address | -| `score` | number | Confidence score | -| `result` | string | Verification result | -| `status` | string | Status message | -| `total` | number | Total results count | -| `personal_emails` | number | Personal emails count | -| `generic_emails` | number | Generic emails count | +| `person` | object | Person information \(undefined for companies_find tool\) | +| `company` | object | Company information including name, domain, industry, size, country, linkedin, and twitter | ### `hunter_email_count` @@ -201,15 +197,11 @@ Returns the total number of email addresses found for a domain or company. | Parameter | Type | Description | | --------- | ---- | ----------- | -| `results` | json | Search results | -| `emails` | json | Email addresses found | -| `email` | string | Found email address | -| `score` | number | Confidence score | -| `result` | string | Verification result | -| `status` | string | Status message | -| `total` | number | Total results count | -| `personal_emails` | number | Personal emails count | -| `generic_emails` | number | Generic emails count | +| `total` | number | Total number of email addresses found | +| `personal_emails` | number | Number of personal email addresses found | +| `generic_emails` | number | Number of generic email addresses found | +| `department` | object | Breakdown of email addresses by department \(executive, it, finance, management, sales, legal, support, hr, marketing, communication\) | +| `seniority` | object | Breakdown of email addresses by seniority level \(junior, senior, executive\) | diff --git a/apps/docs/content/docs/tools/image_generator.mdx b/apps/docs/content/docs/tools/image_generator.mdx index 2614231f44..4fbc3fbb52 100644 --- a/apps/docs/content/docs/tools/image_generator.mdx +++ b/apps/docs/content/docs/tools/image_generator.mdx @@ -73,9 +73,8 @@ Generate images using OpenAI | Parameter | Type | Description | | --------- | ---- | ----------- | -| `content` | string | Generation response | -| `image` | string | Generated image URL | -| `metadata` | json | Generation metadata | +| `success` | boolean | Operation success status | +| `output` | object | Generated image data | diff --git a/apps/docs/content/docs/tools/jina.mdx b/apps/docs/content/docs/tools/jina.mdx index 7daef77f54..f0d0061c34 100644 --- a/apps/docs/content/docs/tools/jina.mdx +++ b/apps/docs/content/docs/tools/jina.mdx @@ -87,7 +87,7 @@ Extract and process web content into clean, LLM-friendly text using Jina AI Read | Parameter | Type | Description | | --------- | ---- | ----------- | -| `content` | string | Extracted content | +| `content` | string | The extracted content from the URL, processed into clean, LLM-friendly text | diff --git a/apps/docs/content/docs/tools/jira.mdx b/apps/docs/content/docs/tools/jira.mdx index 19973d3731..81fabb91cb 100644 --- a/apps/docs/content/docs/tools/jira.mdx +++ b/apps/docs/content/docs/tools/jira.mdx @@ -57,7 +57,6 @@ Retrieve detailed information about a specific Jira issue | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `accessToken` | string | Yes | OAuth access token for Jira | | `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | | `projectId` | string | No | Jira project ID to retrieve issues from. If not provided, all issues will be retrieved. | | `issueKey` | string | Yes | Jira issue key to retrieve \(e.g., PROJ-123\) | @@ -67,14 +66,8 @@ Retrieve detailed information about a specific Jira issue | Parameter | Type | Description | | --------- | ---- | ----------- | -| `ts` | string | Timestamp | -| `issueKey` | string | Issue key | -| `summary` | string | Issue summary | -| `description` | string | Issue description | -| `created` | string | Creation date | -| `updated` | string | Update date | -| `success` | boolean | Operation success | -| `url` | string | Issue URL | +| `success` | boolean | Operation success status | +| `output` | object | Jira issue details with issue key, summary, description, created and updated timestamps | ### `jira_update` @@ -84,7 +77,6 @@ Update a Jira issue | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `accessToken` | string | Yes | OAuth access token for Jira | | `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | | `projectId` | string | No | Jira project ID to update issues in. If not provided, all issues will be retrieved. | | `issueKey` | string | Yes | Jira issue key to update | @@ -99,14 +91,8 @@ Update a Jira issue | Parameter | Type | Description | | --------- | ---- | ----------- | -| `ts` | string | Timestamp | -| `issueKey` | string | Issue key | -| `summary` | string | Issue summary | -| `description` | string | Issue description | -| `created` | string | Creation date | -| `updated` | string | Update date | -| `success` | boolean | Operation success | -| `url` | string | Issue URL | +| `success` | boolean | Operation success status | +| `output` | object | Updated Jira issue details with timestamp, issue key, summary, and success status | ### `jira_write` @@ -116,7 +102,6 @@ Write a Jira issue | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `accessToken` | string | Yes | OAuth access token for Jira | | `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | | `projectId` | string | Yes | Project ID for the issue | | `summary` | string | Yes | Summary for the issue | @@ -130,14 +115,8 @@ Write a Jira issue | Parameter | Type | Description | | --------- | ---- | ----------- | -| `ts` | string | Timestamp | -| `issueKey` | string | Issue key | -| `summary` | string | Issue summary | -| `description` | string | Issue description | -| `created` | string | Creation date | -| `updated` | string | Update date | -| `success` | boolean | Operation success | -| `url` | string | Issue URL | +| `success` | boolean | Operation success status | +| `output` | object | Created Jira issue details with timestamp, issue key, summary, success status, and URL | ### `jira_bulk_read` @@ -147,7 +126,6 @@ Retrieve multiple Jira issues in bulk | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `accessToken` | string | Yes | OAuth access token for Jira | | `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | | `projectId` | string | Yes | Jira project ID | | `cloudId` | string | No | Jira cloud ID | @@ -156,14 +134,8 @@ Retrieve multiple Jira issues in bulk | Parameter | Type | Description | | --------- | ---- | ----------- | -| `ts` | string | Timestamp | -| `issueKey` | string | Issue key | -| `summary` | string | Issue summary | -| `description` | string | Issue description | -| `created` | string | Creation date | -| `updated` | string | Update date | -| `success` | boolean | Operation success | -| `url` | string | Issue URL | +| `success` | boolean | Operation success status | +| `output` | array | Array of Jira issues with summary, description, created and updated timestamps | diff --git a/apps/docs/content/docs/tools/knowledge.mdx b/apps/docs/content/docs/tools/knowledge.mdx index 5a1b4e6b35..525f3f8cc3 100644 --- a/apps/docs/content/docs/tools/knowledge.mdx +++ b/apps/docs/content/docs/tools/knowledge.mdx @@ -72,9 +72,7 @@ Search for similar content in a knowledge base using vector similarity | Parameter | Type | Description | | --------- | ---- | ----------- | -| `results` | json | Search results | -| `query` | string | Query used | -| `totalResults` | number | Total results count | +| `results` | array | Array of search results from the knowledge base | ### `knowledge_upload_chunk` @@ -92,9 +90,7 @@ Upload a new chunk to a document in a knowledge base | Parameter | Type | Description | | --------- | ---- | ----------- | -| `results` | json | Search results | -| `query` | string | Query used | -| `totalResults` | number | Total results count | +| `data` | object | Information about the uploaded chunk | ### `knowledge_create_document` @@ -120,9 +116,7 @@ Create a new document in a knowledge base | Parameter | Type | Description | | --------- | ---- | ----------- | -| `results` | json | Search results | -| `query` | string | Query used | -| `totalResults` | number | Total results count | +| `data` | object | Information about the created document | diff --git a/apps/docs/content/docs/tools/linear.mdx b/apps/docs/content/docs/tools/linear.mdx index a541add5a0..3da9fc927b 100644 --- a/apps/docs/content/docs/tools/linear.mdx +++ b/apps/docs/content/docs/tools/linear.mdx @@ -63,8 +63,7 @@ Fetch and filter issues from Linear | Parameter | Type | Description | | --------- | ---- | ----------- | -| `issues` | json | Issues list | -| `issue` | json | Single issue data | +| `issues` | array | Array of issues from the specified Linear team and project, each containing id, title, description, state, teamId, and projectId | ### `linear_create_issue` @@ -83,8 +82,7 @@ Create a new issue in Linear | Parameter | Type | Description | | --------- | ---- | ----------- | -| `issues` | json | Issues list | -| `issue` | json | Single issue data | +| `issue` | object | The created issue containing id, title, description, state, teamId, and projectId | diff --git a/apps/docs/content/docs/tools/linkup.mdx b/apps/docs/content/docs/tools/linkup.mdx index 24c15bb1a9..52c67c9e2e 100644 --- a/apps/docs/content/docs/tools/linkup.mdx +++ b/apps/docs/content/docs/tools/linkup.mdx @@ -58,16 +58,16 @@ Search the web for information using Linkup | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `q` | string | Yes | The search query | -| `depth` | string | Yes | Search depth \(has to either be | -| `outputType` | string | Yes | Type of output to return \(has to either be | +| `depth` | string | Yes | Search depth \(has to either be "standard" or "deep"\) | +| `outputType` | string | Yes | Type of output to return \(has to either be "sourcedAnswer" or "searchResults"\) | | `apiKey` | string | Yes | Enter your Linkup API key | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `answer` | string | Generated answer | -| `sources` | json | Source references | +| `answer` | string | The sourced answer to the search query | +| `sources` | array | Array of sources used to compile the answer, each containing name, url, and snippet | diff --git a/apps/docs/content/docs/tools/mem0.mdx b/apps/docs/content/docs/tools/mem0.mdx index 6c9448d051..442cf95069 100644 --- a/apps/docs/content/docs/tools/mem0.mdx +++ b/apps/docs/content/docs/tools/mem0.mdx @@ -66,9 +66,8 @@ Add memories to Mem0 for persistent storage and retrieval | Parameter | Type | Description | | --------- | ---- | ----------- | -| `ids` | any | Memory identifiers | -| `memories` | any | Memory data | -| `searchResults` | any | Search results | +| `ids` | array | Array of memory IDs that were created | +| `memories` | array | Array of memory objects that were created | ### `mem0_search_memories` @@ -87,9 +86,8 @@ Search for memories in Mem0 using semantic search | Parameter | Type | Description | | --------- | ---- | ----------- | -| `ids` | any | Memory identifiers | -| `memories` | any | Memory data | -| `searchResults` | any | Search results | +| `searchResults` | array | Array of search results with memory data, each containing id, data, and score | +| `ids` | array | Array of memory IDs found in the search results | ### `mem0_get_memories` @@ -110,9 +108,8 @@ Retrieve memories from Mem0 by ID or filter criteria | Parameter | Type | Description | | --------- | ---- | ----------- | -| `ids` | any | Memory identifiers | -| `memories` | any | Memory data | -| `searchResults` | any | Search results | +| `memories` | array | Array of retrieved memory objects | +| `ids` | array | Array of memory IDs that were retrieved | diff --git a/apps/docs/content/docs/tools/memory.mdx b/apps/docs/content/docs/tools/memory.mdx index fcd46da35a..e9324e0a65 100644 --- a/apps/docs/content/docs/tools/memory.mdx +++ b/apps/docs/content/docs/tools/memory.mdx @@ -57,8 +57,9 @@ Add a new memory to the database or append to existing memory with the same ID. | Parameter | Type | Description | | --------- | ---- | ----------- | -| `memories` | any | Memory data | -| `id` | string | Memory identifier | +| `success` | boolean | Whether the memory was added successfully | +| `memories` | array | Array of memory objects including the new or updated memory | +| `error` | string | Error message if operation failed | ### `memory_get` @@ -74,8 +75,10 @@ Retrieve a specific memory by its ID | Parameter | Type | Description | | --------- | ---- | ----------- | -| `memories` | any | Memory data | -| `id` | string | Memory identifier | +| `success` | boolean | Whether the memory was retrieved successfully | +| `memories` | array | Array of memory data for the requested ID | +| `message` | string | Success or error message | +| `error` | string | Error message if operation failed | ### `memory_get_all` @@ -90,8 +93,10 @@ Retrieve all memories from the database | Parameter | Type | Description | | --------- | ---- | ----------- | -| `memories` | any | Memory data | -| `id` | string | Memory identifier | +| `success` | boolean | Whether all memories were retrieved successfully | +| `memories` | array | Array of all memory objects with keys, types, and data | +| `message` | string | Success or error message | +| `error` | string | Error message if operation failed | ### `memory_delete` @@ -107,8 +112,9 @@ Delete a specific memory by its ID | Parameter | Type | Description | | --------- | ---- | ----------- | -| `memories` | any | Memory data | -| `id` | string | Memory identifier | +| `success` | boolean | Whether the memory was deleted successfully | +| `message` | string | Success or error message | +| `error` | string | Error message if operation failed | diff --git a/apps/docs/content/docs/tools/meta.json b/apps/docs/content/docs/tools/meta.json index b4ba8f9277..c954de07fd 100644 --- a/apps/docs/content/docs/tools/meta.json +++ b/apps/docs/content/docs/tools/meta.json @@ -11,6 +11,7 @@ "exa", "file", "firecrawl", + "generic_webhook", "github", "gmail", "google_calendar", diff --git a/apps/docs/content/docs/tools/microsoft_excel.mdx b/apps/docs/content/docs/tools/microsoft_excel.mdx index 15ecd8e28d..38b37827e6 100644 --- a/apps/docs/content/docs/tools/microsoft_excel.mdx +++ b/apps/docs/content/docs/tools/microsoft_excel.mdx @@ -108,7 +108,6 @@ Read data from a Microsoft Excel spreadsheet | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `accessToken` | string | Yes | The access token for the Microsoft Excel API | | `spreadsheetId` | string | Yes | The ID of the spreadsheet to read from | | `range` | string | No | The range of cells to read from | @@ -116,14 +115,8 @@ Read data from a Microsoft Excel spreadsheet | Parameter | Type | Description | | --------- | ---- | ----------- | -| `data` | json | Sheet data | -| `metadata` | json | Operation metadata | -| `updatedRange` | string | Updated range | -| `updatedRows` | number | Updated rows count | -| `updatedColumns` | number | Updated columns count | -| `updatedCells` | number | Updated cells count | -| `index` | number | Row index | -| `values` | json | Table values | +| `success` | boolean | Operation success status | +| `output` | object | Excel spreadsheet data and metadata | ### `microsoft_excel_write` @@ -133,7 +126,6 @@ Write data to a Microsoft Excel spreadsheet | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `accessToken` | string | Yes | The access token for the Microsoft Excel API | | `spreadsheetId` | string | Yes | The ID of the spreadsheet to write to | | `range` | string | No | The range of cells to write to | | `values` | array | Yes | The data to write to the spreadsheet | @@ -144,14 +136,8 @@ Write data to a Microsoft Excel spreadsheet | Parameter | Type | Description | | --------- | ---- | ----------- | -| `data` | json | Sheet data | -| `metadata` | json | Operation metadata | -| `updatedRange` | string | Updated range | -| `updatedRows` | number | Updated rows count | -| `updatedColumns` | number | Updated columns count | -| `updatedCells` | number | Updated cells count | -| `index` | number | Row index | -| `values` | json | Table values | +| `success` | boolean | Operation success status | +| `output` | object | Write operation results and metadata | ### `microsoft_excel_table_add` @@ -161,7 +147,6 @@ Add new rows to a Microsoft Excel table | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `accessToken` | string | Yes | The access token for the Microsoft Excel API | | `spreadsheetId` | string | Yes | The ID of the spreadsheet containing the table | | `tableName` | string | Yes | The name of the table to add rows to | | `values` | array | Yes | The data to add to the table \(array of arrays or array of objects\) | @@ -170,14 +155,8 @@ Add new rows to a Microsoft Excel table | Parameter | Type | Description | | --------- | ---- | ----------- | -| `data` | json | Sheet data | -| `metadata` | json | Operation metadata | -| `updatedRange` | string | Updated range | -| `updatedRows` | number | Updated rows count | -| `updatedColumns` | number | Updated columns count | -| `updatedCells` | number | Updated cells count | -| `index` | number | Row index | -| `values` | json | Table values | +| `success` | boolean | Operation success status | +| `output` | object | Table add operation results and metadata | diff --git a/apps/docs/content/docs/tools/microsoft_planner.mdx b/apps/docs/content/docs/tools/microsoft_planner.mdx index 5ae82298f3..08b4bc2dbd 100644 --- a/apps/docs/content/docs/tools/microsoft_planner.mdx +++ b/apps/docs/content/docs/tools/microsoft_planner.mdx @@ -136,7 +136,6 @@ Read tasks from Microsoft Planner - get all user tasks or all tasks from a speci | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `accessToken` | string | Yes | The access token for the Microsoft Planner API | | `planId` | string | No | The ID of the plan to get tasks from \(if not provided, gets all user tasks\) | | `taskId` | string | No | The ID of the task to get | @@ -144,8 +143,9 @@ Read tasks from Microsoft Planner - get all user tasks or all tasks from a speci | Parameter | Type | Description | | --------- | ---- | ----------- | -| `task` | json | The Microsoft Planner task object, including details such as id, title, description, status, due date, and assignees. | -| `metadata` | json | Additional metadata about the operation, such as timestamps, request status, or other relevant information. | +| `success` | boolean | Whether tasks were retrieved successfully | +| `tasks` | array | Array of task objects with filtered properties | +| `metadata` | object | Metadata including planId, userId, and planUrl | ### `microsoft_planner_create_task` @@ -155,7 +155,6 @@ Create a new task in Microsoft Planner | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `accessToken` | string | Yes | The access token for the Microsoft Planner API | | `planId` | string | Yes | The ID of the plan where the task will be created | | `title` | string | Yes | The title of the task | | `description` | string | No | The description of the task | @@ -167,8 +166,9 @@ Create a new task in Microsoft Planner | Parameter | Type | Description | | --------- | ---- | ----------- | -| `task` | json | The Microsoft Planner task object, including details such as id, title, description, status, due date, and assignees. | -| `metadata` | json | Additional metadata about the operation, such as timestamps, request status, or other relevant information. | +| `success` | boolean | Whether the task was created successfully | +| `task` | object | The created task object with all properties | +| `metadata` | object | Metadata including planId, taskId, and taskUrl | diff --git a/apps/docs/content/docs/tools/microsoft_teams.mdx b/apps/docs/content/docs/tools/microsoft_teams.mdx index 51e80b0113..81ae2e4b51 100644 --- a/apps/docs/content/docs/tools/microsoft_teams.mdx +++ b/apps/docs/content/docs/tools/microsoft_teams.mdx @@ -112,16 +112,19 @@ Read content from a Microsoft Teams chat | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `accessToken` | string | Yes | The access token for the Microsoft Teams API | | `chatId` | string | Yes | The ID of the chat to read from | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `content` | string | Message content | -| `metadata` | json | Message metadata | -| `updatedContent` | boolean | Content update status | +| `success` | boolean | Teams chat read operation success status | +| `messageCount` | number | Number of messages retrieved from chat | +| `chatId` | string | ID of the chat that was read from | +| `messages` | array | Array of chat message objects | +| `attachmentCount` | number | Total number of attachments found | +| `attachmentTypes` | array | Types of attachments found | +| `content` | string | Formatted content of chat messages | ### `microsoft_teams_write_chat` @@ -131,7 +134,6 @@ Write or update content in a Microsoft Teams chat | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `accessToken` | string | Yes | The access token for the Microsoft Teams API | | `chatId` | string | Yes | The ID of the chat to write to | | `content` | string | Yes | The content to write to the message | @@ -139,9 +141,12 @@ Write or update content in a Microsoft Teams chat | Parameter | Type | Description | | --------- | ---- | ----------- | -| `content` | string | Message content | -| `metadata` | json | Message metadata | -| `updatedContent` | boolean | Content update status | +| `success` | boolean | Teams chat message send success status | +| `messageId` | string | Unique identifier for the sent message | +| `chatId` | string | ID of the chat where message was sent | +| `createdTime` | string | Timestamp when message was created | +| `url` | string | Web URL to the message | +| `updatedContent` | boolean | Whether content was successfully updated | ### `microsoft_teams_read_channel` @@ -151,7 +156,6 @@ Read content from a Microsoft Teams channel | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `accessToken` | string | Yes | The access token for the Microsoft Teams API | | `teamId` | string | Yes | The ID of the team to read from | | `channelId` | string | Yes | The ID of the channel to read from | @@ -159,9 +163,14 @@ Read content from a Microsoft Teams channel | Parameter | Type | Description | | --------- | ---- | ----------- | -| `content` | string | Message content | -| `metadata` | json | Message metadata | -| `updatedContent` | boolean | Content update status | +| `success` | boolean | Teams channel read operation success status | +| `messageCount` | number | Number of messages retrieved from channel | +| `teamId` | string | ID of the team that was read from | +| `channelId` | string | ID of the channel that was read from | +| `messages` | array | Array of channel message objects | +| `attachmentCount` | number | Total number of attachments found | +| `attachmentTypes` | array | Types of attachments found | +| `content` | string | Formatted content of channel messages | ### `microsoft_teams_write_channel` @@ -171,7 +180,6 @@ Write or send a message to a Microsoft Teams channel | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `accessToken` | string | Yes | The access token for the Microsoft Teams API | | `teamId` | string | Yes | The ID of the team to write to | | `channelId` | string | Yes | The ID of the channel to write to | | `content` | string | Yes | The content to write to the channel | @@ -180,9 +188,13 @@ Write or send a message to a Microsoft Teams channel | Parameter | Type | Description | | --------- | ---- | ----------- | -| `content` | string | Message content | -| `metadata` | json | Message metadata | -| `updatedContent` | boolean | Content update status | +| `success` | boolean | Teams channel message send success status | +| `messageId` | string | Unique identifier for the sent message | +| `teamId` | string | ID of the team where message was sent | +| `channelId` | string | ID of the channel where message was sent | +| `createdTime` | string | Timestamp when message was created | +| `url` | string | Web URL to the message | +| `updatedContent` | boolean | Whether content was successfully updated | diff --git a/apps/docs/content/docs/tools/mistral_parse.mdx b/apps/docs/content/docs/tools/mistral_parse.mdx index dbd5a0f72f..08fadae49f 100644 --- a/apps/docs/content/docs/tools/mistral_parse.mdx +++ b/apps/docs/content/docs/tools/mistral_parse.mdx @@ -106,8 +106,9 @@ Parse PDF documents using Mistral OCR API | Parameter | Type | Description | | --------- | ---- | ----------- | -| `content` | string | Extracted content | -| `metadata` | json | Processing metadata | +| `success` | boolean | Whether the PDF was parsed successfully | +| `content` | string | Extracted content in the requested format \(markdown, text, or JSON\) | +| `metadata` | object | Processing metadata including jobId, fileType, pageCount, and usage info | diff --git a/apps/docs/content/docs/tools/notion.mdx b/apps/docs/content/docs/tools/notion.mdx index 51801ce370..5b117623ac 100644 --- a/apps/docs/content/docs/tools/notion.mdx +++ b/apps/docs/content/docs/tools/notion.mdx @@ -59,15 +59,14 @@ Read content from a Notion page | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `accessToken` | string | Yes | Notion OAuth access token | | `pageId` | string | Yes | The ID of the Notion page to read | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `content` | string | Page content | -| `metadata` | any | Page metadata | +| `content` | string | Page content in markdown format with headers, paragraphs, lists, and todos | +| `metadata` | object | Page metadata including title, URL, and timestamps | ### `notion_read_database` @@ -77,15 +76,14 @@ Read database information and structure from Notion | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `accessToken` | string | Yes | Notion OAuth access token | | `databaseId` | string | Yes | The ID of the Notion database to read | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `content` | string | Page content | -| `metadata` | any | Page metadata | +| `content` | string | Database information including title, properties schema, and metadata | +| `metadata` | object | Database metadata including title, ID, URL, timestamps, and properties schema | ### `notion_write` @@ -95,7 +93,6 @@ Append content to a Notion page | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `accessToken` | string | Yes | Notion OAuth access token | | `pageId` | string | Yes | The ID of the Notion page to append content to | | `content` | string | Yes | The content to append to the page | @@ -103,8 +100,7 @@ Append content to a Notion page | Parameter | Type | Description | | --------- | ---- | ----------- | -| `content` | string | Page content | -| `metadata` | any | Page metadata | +| `content` | string | Success message confirming content was appended to page | ### `notion_create_page` @@ -114,7 +110,6 @@ Create a new page in Notion | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `accessToken` | string | Yes | Notion OAuth access token | | `parentId` | string | Yes | ID of the parent page | | `title` | string | No | Title of the new page | | `content` | string | No | Optional content to add to the page upon creation | @@ -123,8 +118,8 @@ Create a new page in Notion | Parameter | Type | Description | | --------- | ---- | ----------- | -| `content` | string | Page content | -| `metadata` | any | Page metadata | +| `content` | string | Success message confirming page creation | +| `metadata` | object | Page metadata including title, page ID, URL, and timestamps | ### `notion_query_database` @@ -134,7 +129,6 @@ Query and filter Notion database entries with advanced filtering | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `accessToken` | string | Yes | Notion OAuth access token | | `databaseId` | string | Yes | The ID of the database to query | | `filter` | string | No | Filter conditions as JSON \(optional\) | | `sorts` | string | No | Sort criteria as JSON array \(optional\) | @@ -144,8 +138,8 @@ Query and filter Notion database entries with advanced filtering | Parameter | Type | Description | | --------- | ---- | ----------- | -| `content` | string | Page content | -| `metadata` | any | Page metadata | +| `content` | string | Formatted list of database entries with their properties | +| `metadata` | object | Query metadata including total results count, pagination info, and raw results array | ### `notion_search` @@ -155,7 +149,6 @@ Search across all pages and databases in Notion workspace | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `accessToken` | string | Yes | Notion OAuth access token | | `query` | string | No | Search terms \(leave empty to get all pages\) | | `filterType` | string | No | Filter by object type: page, database, or leave empty for all | | `pageSize` | number | No | Number of results to return \(default: 100, max: 100\) | @@ -164,8 +157,8 @@ Search across all pages and databases in Notion workspace | Parameter | Type | Description | | --------- | ---- | ----------- | -| `content` | string | Page content | -| `metadata` | any | Page metadata | +| `content` | string | Formatted list of search results including pages and databases | +| `metadata` | object | Search metadata including total results count, pagination info, and raw results array | ### `notion_create_database` @@ -175,17 +168,16 @@ Create a new database in Notion with custom properties | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `accessToken` | string | Yes | Notion OAuth access token | | `parentId` | string | Yes | ID of the parent page where the database will be created | | `title` | string | Yes | Title for the new database | -| `properties` | string | No | Database properties as JSON object \(optional, will create a default | +| `properties` | string | No | Database properties as JSON object \(optional, will create a default "Name" property if empty\) | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `content` | string | Page content | -| `metadata` | any | Page metadata | +| `content` | string | Success message with database details and properties list | +| `metadata` | object | Database metadata including ID, title, URL, creation time, and properties schema | diff --git a/apps/docs/content/docs/tools/onedrive.mdx b/apps/docs/content/docs/tools/onedrive.mdx index 7a389f238e..1708434f06 100644 --- a/apps/docs/content/docs/tools/onedrive.mdx +++ b/apps/docs/content/docs/tools/onedrive.mdx @@ -65,7 +65,6 @@ Upload a file to OneDrive | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `accessToken` | string | Yes | The access token for the OneDrive API | | `fileName` | string | Yes | The name of the file to upload | | `content` | string | Yes | The content of the file to upload | | `folderSelector` | string | No | Select the folder to upload the file to | @@ -75,8 +74,8 @@ Upload a file to OneDrive | Parameter | Type | Description | | --------- | ---- | ----------- | -| `file` | json | The OneDrive file object, including details such as id, name, size, and more. | -| `files` | json | An array of OneDrive file objects, each containing details such as id, name, size, and more. | +| `success` | boolean | Whether the file was uploaded successfully | +| `file` | object | The uploaded file object with metadata including id, name, webViewLink, webContentLink, and timestamps | ### `onedrive_create_folder` @@ -86,7 +85,6 @@ Create a new folder in OneDrive | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `accessToken` | string | Yes | The access token for the OneDrive API | | `folderName` | string | Yes | Name of the folder to create | | `folderSelector` | string | No | Select the parent folder to create the folder in | | `folderId` | string | No | ID of the parent folder \(internal use\) | @@ -95,8 +93,8 @@ Create a new folder in OneDrive | Parameter | Type | Description | | --------- | ---- | ----------- | -| `file` | json | The OneDrive file object, including details such as id, name, size, and more. | -| `files` | json | An array of OneDrive file objects, each containing details such as id, name, size, and more. | +| `success` | boolean | Whether the folder was created successfully | +| `file` | object | The created folder object with metadata including id, name, webViewLink, and timestamps | ### `onedrive_list` @@ -106,7 +104,6 @@ List files and folders in OneDrive | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `accessToken` | string | Yes | The access token for the OneDrive API | | `folderSelector` | string | No | Select the folder to list files from | | `folderId` | string | No | The ID of the folder to list files from \(internal use\) | | `query` | string | No | A query to filter the files | @@ -116,8 +113,9 @@ List files and folders in OneDrive | Parameter | Type | Description | | --------- | ---- | ----------- | -| `file` | json | The OneDrive file object, including details such as id, name, size, and more. | -| `files` | json | An array of OneDrive file objects, each containing details such as id, name, size, and more. | +| `success` | boolean | Whether files were listed successfully | +| `files` | array | Array of file and folder objects with metadata | +| `nextPageToken` | string | Token for retrieving the next page of results \(optional\) | diff --git a/apps/docs/content/docs/tools/openai.mdx b/apps/docs/content/docs/tools/openai.mdx index b8d04338bd..f445f359dd 100644 --- a/apps/docs/content/docs/tools/openai.mdx +++ b/apps/docs/content/docs/tools/openai.mdx @@ -66,9 +66,8 @@ Generate embeddings from text using OpenAI | Parameter | Type | Description | | --------- | ---- | ----------- | -| `embeddings` | json | Generated embeddings | -| `model` | string | Model used | -| `usage` | json | Token usage | +| `success` | boolean | Operation success status | +| `output` | object | Embeddings generation results | diff --git a/apps/docs/content/docs/tools/outlook.mdx b/apps/docs/content/docs/tools/outlook.mdx index 1aefc7e077..f70725f137 100644 --- a/apps/docs/content/docs/tools/outlook.mdx +++ b/apps/docs/content/docs/tools/outlook.mdx @@ -154,7 +154,6 @@ Send emails using Outlook | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `accessToken` | string | Yes | Access token for Outlook API | | `to` | string | Yes | Recipient email address | | `subject` | string | Yes | Email subject | | `body` | string | Yes | Email body content | @@ -167,8 +166,10 @@ Send emails using Outlook | Parameter | Type | Description | | --------- | ---- | ----------- | -| `message` | string | Response message | -| `results` | json | Email results | +| `success` | boolean | Email send success status | +| `status` | string | Delivery status of the email | +| `timestamp` | string | Timestamp when email was sent | +| `message` | string | Success or error message | ### `outlook_draft` @@ -178,17 +179,22 @@ Draft emails using Outlook | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `accessToken` | string | Yes | Access token for Outlook API | | `to` | string | Yes | Recipient email address | | `subject` | string | Yes | Email subject | | `body` | string | Yes | Email body content | +| `cc` | string | No | CC recipients \(comma-separated\) | +| `bcc` | string | No | BCC recipients \(comma-separated\) | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `message` | string | Response message | -| `results` | json | Email results | +| `success` | boolean | Email draft creation success status | +| `messageId` | string | Unique identifier for the drafted email | +| `status` | string | Draft status of the email | +| `subject` | string | Subject of the drafted email | +| `timestamp` | string | Timestamp when draft was created | +| `message` | string | Success or error message | ### `outlook_read` @@ -198,7 +204,6 @@ Read emails from Outlook | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `accessToken` | string | Yes | OAuth access token for Outlook | | `folder` | string | No | Folder ID to read emails from \(default: Inbox\) | | `maxResults` | number | No | Maximum number of emails to retrieve \(default: 1, max: 10\) | @@ -206,8 +211,10 @@ Read emails from Outlook | Parameter | Type | Description | | --------- | ---- | ----------- | -| `message` | string | Response message | -| `results` | json | Email results | +| `success` | boolean | Email read operation success status | +| `messageCount` | number | Number of emails retrieved | +| `messages` | array | Array of email message objects | +| `message` | string | Success or status message | diff --git a/apps/docs/content/docs/tools/perplexity.mdx b/apps/docs/content/docs/tools/perplexity.mdx index b8855fdde2..ed690490f9 100644 --- a/apps/docs/content/docs/tools/perplexity.mdx +++ b/apps/docs/content/docs/tools/perplexity.mdx @@ -62,9 +62,8 @@ Generate completions using Perplexity AI chat models | Parameter | Type | Description | | --------- | ---- | ----------- | -| `content` | string | Generated response | -| `model` | string | Model used | -| `usage` | json | Token usage | +| `success` | boolean | Operation success status | +| `output` | object | Chat completion results | diff --git a/apps/docs/content/docs/tools/pinecone.mdx b/apps/docs/content/docs/tools/pinecone.mdx index 76baccb1ae..6eb76e2025 100644 --- a/apps/docs/content/docs/tools/pinecone.mdx +++ b/apps/docs/content/docs/tools/pinecone.mdx @@ -67,12 +67,10 @@ Generate embeddings from text using Pinecone | Parameter | Type | Description | | --------- | ---- | ----------- | -| `matches` | any | Search matches | -| `upsertedCount` | any | Upserted count | -| `data` | any | Response data | -| `model` | any | Model information | -| `vector_type` | any | Vector type | -| `usage` | any | Usage statistics | +| `data` | array | Generated embeddings data with values and vector type | +| `model` | string | Model used for generating embeddings | +| `vector_type` | string | Type of vector generated \(dense/sparse\) | +| `usage` | object | Usage statistics for embeddings generation | ### `pinecone_upsert_text` @@ -91,12 +89,8 @@ Insert or update text records in a Pinecone index | Parameter | Type | Description | | --------- | ---- | ----------- | -| `matches` | any | Search matches | -| `upsertedCount` | any | Upserted count | -| `data` | any | Response data | -| `model` | any | Model information | -| `vector_type` | any | Vector type | -| `usage` | any | Usage statistics | +| `statusText` | string | Status of the upsert operation | +| `upsertedCount` | number | Number of records successfully upserted | ### `pinecone_search_text` @@ -119,12 +113,7 @@ Search for similar text in a Pinecone index | Parameter | Type | Description | | --------- | ---- | ----------- | -| `matches` | any | Search matches | -| `upsertedCount` | any | Upserted count | -| `data` | any | Response data | -| `model` | any | Model information | -| `vector_type` | any | Vector type | -| `usage` | any | Usage statistics | +| `matches` | array | Search results with ID, score, and metadata | ### `pinecone_search_vector` @@ -147,12 +136,8 @@ Search for similar vectors in a Pinecone index | Parameter | Type | Description | | --------- | ---- | ----------- | -| `matches` | any | Search matches | -| `upsertedCount` | any | Upserted count | -| `data` | any | Response data | -| `model` | any | Model information | -| `vector_type` | any | Vector type | -| `usage` | any | Usage statistics | +| `matches` | array | Vector search results with ID, score, values, and metadata | +| `namespace` | string | Namespace where the search was performed | ### `pinecone_fetch` @@ -171,12 +156,7 @@ Fetch vectors by ID from a Pinecone index | Parameter | Type | Description | | --------- | ---- | ----------- | -| `matches` | any | Search matches | -| `upsertedCount` | any | Upserted count | -| `data` | any | Response data | -| `model` | any | Model information | -| `vector_type` | any | Vector type | -| `usage` | any | Usage statistics | +| `matches` | array | Fetched vectors with ID, values, metadata, and score | diff --git a/apps/docs/content/docs/tools/qdrant.mdx b/apps/docs/content/docs/tools/qdrant.mdx index fcc2c478bf..44a7719da4 100644 --- a/apps/docs/content/docs/tools/qdrant.mdx +++ b/apps/docs/content/docs/tools/qdrant.mdx @@ -126,10 +126,8 @@ Insert or update points in a Qdrant collection | Parameter | Type | Description | | --------- | ---- | ----------- | -| `matches` | any | Search matches | -| `upsertedCount` | any | Upserted count | -| `data` | any | Response data | -| `status` | any | Operation status | +| `status` | string | Status of the upsert operation | +| `data` | object | Result data from the upsert operation | ### `qdrant_search_vector` @@ -152,10 +150,8 @@ Search for similar vectors in a Qdrant collection | Parameter | Type | Description | | --------- | ---- | ----------- | -| `matches` | any | Search matches | -| `upsertedCount` | any | Upserted count | -| `data` | any | Response data | -| `status` | any | Operation status | +| `data` | array | Vector search results with ID, score, payload, and optional vector data | +| `status` | string | Status of the search operation | ### `qdrant_fetch_points` @@ -176,10 +172,8 @@ Fetch points by ID from a Qdrant collection | Parameter | Type | Description | | --------- | ---- | ----------- | -| `matches` | any | Search matches | -| `upsertedCount` | any | Upserted count | -| `data` | any | Response data | -| `status` | any | Operation status | +| `data` | array | Fetched points with ID, payload, and optional vector data | +| `status` | string | Status of the fetch operation | diff --git a/apps/docs/content/docs/tools/reddit.mdx b/apps/docs/content/docs/tools/reddit.mdx index 22d16546a8..851d4c7593 100644 --- a/apps/docs/content/docs/tools/reddit.mdx +++ b/apps/docs/content/docs/tools/reddit.mdx @@ -53,20 +53,17 @@ Fetch posts from a subreddit with different sorting options | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `accessToken` | string | Yes | Access token for Reddit API | | `subreddit` | string | Yes | The name of the subreddit to fetch posts from \(without the r/ prefix\) | -| `sort` | string | No | Sort method for posts: | +| `sort` | string | No | Sort method for posts: "hot", "new", "top", or "rising" \(default: "hot"\) | | `limit` | number | No | Maximum number of posts to return \(default: 10, max: 100\) | -| `time` | string | No | Time filter for | +| `time` | string | No | Time filter for "top" sorted posts: "day", "week", "month", "year", or "all" \(default: "day"\) | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `subreddit` | string | Subreddit name | -| `posts` | json | Posts data | -| `post` | json | Single post data | -| `comments` | json | Comments data | +| `subreddit` | string | Name of the subreddit where posts were fetched from | +| `posts` | array | Array of posts with title, author, URL, score, comments count, and metadata | ### `reddit_get_comments` @@ -76,20 +73,16 @@ Fetch comments from a specific Reddit post | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `accessToken` | string | Yes | Access token for Reddit API | | `postId` | string | Yes | The ID of the Reddit post to fetch comments from | | `subreddit` | string | Yes | The subreddit where the post is located \(without the r/ prefix\) | -| `sort` | string | No | Sort method for comments: | +| `sort` | string | No | Sort method for comments: "confidence", "top", "new", "controversial", "old", "random", "qa" \(default: "confidence"\) | | `limit` | number | No | Maximum number of comments to return \(default: 50, max: 100\) | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `subreddit` | string | Subreddit name | -| `posts` | json | Posts data | -| `post` | json | Single post data | -| `comments` | json | Comments data | +| `post` | object | Post information including ID, title, author, content, and metadata | diff --git a/apps/docs/content/docs/tools/s3.mdx b/apps/docs/content/docs/tools/s3.mdx index f8d62e2f7b..b6955bafae 100644 --- a/apps/docs/content/docs/tools/s3.mdx +++ b/apps/docs/content/docs/tools/s3.mdx @@ -84,8 +84,8 @@ Retrieve an object from an AWS S3 bucket | Parameter | Type | Description | | --------- | ---- | ----------- | -| `url` | string | Presigned URL | -| `metadata` | json | Object metadata | +| `url` | string | Pre-signed URL for downloading the S3 object | +| `metadata` | object | File metadata including type, size, name, and last modified date | diff --git a/apps/docs/content/docs/tools/serper.mdx b/apps/docs/content/docs/tools/serper.mdx index e04ce7b3a7..73b551c099 100644 --- a/apps/docs/content/docs/tools/serper.mdx +++ b/apps/docs/content/docs/tools/serper.mdx @@ -103,7 +103,7 @@ A powerful web search tool that provides access to Google search results through | Parameter | Type | Description | | --------- | ---- | ----------- | -| `searchResults` | json | Search results data | +| `searchResults` | array | Search results with titles, links, snippets, and type-specific metadata \(date for news, rating for places, imageUrl for images\) | diff --git a/apps/docs/content/docs/tools/sharepoint.mdx b/apps/docs/content/docs/tools/sharepoint.mdx index 3c44e35104..e08cbf903f 100644 --- a/apps/docs/content/docs/tools/sharepoint.mdx +++ b/apps/docs/content/docs/tools/sharepoint.mdx @@ -75,7 +75,6 @@ Create a new page in a SharePoint site | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `accessToken` | string | Yes | The access token for the SharePoint API | | `siteId` | string | No | The ID of the SharePoint site \(internal use\) | | `siteSelector` | string | No | Select the SharePoint site | | `pageName` | string | Yes | The name of the page to create | @@ -86,7 +85,7 @@ Create a new page in a SharePoint site | Parameter | Type | Description | | --------- | ---- | ----------- | -| `sites` | json | An array of SharePoint site objects, each containing details such as id, name, and more. | +| `page` | object | Created SharePoint page information | ### `sharepoint_read_page` @@ -96,7 +95,6 @@ Read a specific page from a SharePoint site | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `accessToken` | string | Yes | The access token for the SharePoint API | | `siteSelector` | string | No | Select the SharePoint site | | `siteId` | string | No | The ID of the SharePoint site \(internal use\) | | `pageId` | string | No | The ID of the page to read | @@ -107,7 +105,7 @@ Read a specific page from a SharePoint site | Parameter | Type | Description | | --------- | ---- | ----------- | -| `sites` | json | An array of SharePoint site objects, each containing details such as id, name, and more. | +| `page` | object | Information about the SharePoint page | ### `sharepoint_list_sites` @@ -117,7 +115,6 @@ List details of all SharePoint sites | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `accessToken` | string | Yes | The access token for the SharePoint API | | `siteSelector` | string | No | Select the SharePoint site | | `groupId` | string | No | The group ID for accessing a group team site | @@ -125,7 +122,7 @@ List details of all SharePoint sites | Parameter | Type | Description | | --------- | ---- | ----------- | -| `sites` | json | An array of SharePoint site objects, each containing details such as id, name, and more. | +| `site` | object | Information about the current SharePoint site | diff --git a/apps/docs/content/docs/tools/slack.mdx b/apps/docs/content/docs/tools/slack.mdx index 074e9c1d18..68c4ac50f3 100644 --- a/apps/docs/content/docs/tools/slack.mdx +++ b/apps/docs/content/docs/tools/slack.mdx @@ -1,6 +1,6 @@ --- title: Slack -description: Send messages to Slack +description: Send messages to Slack or trigger workflows from Slack events --- import { BlockInfoCard } from "@/components/ui/block-info-card" @@ -64,7 +64,7 @@ This allows for powerful automation scenarios such as sending notifications, ale ## Usage Instructions -Comprehensive Slack integration with OAuth authentication. Send formatted messages using Slack's mrkdwn syntax. +Comprehensive Slack integration with OAuth authentication. Send formatted messages using Slack's mrkdwn syntax or trigger workflows from Slack events like mentions and messages. @@ -80,7 +80,6 @@ Send messages to Slack channels or users through the Slack API. Supports Slack m | --------- | ---- | -------- | ----------- | | `authMethod` | string | No | Authentication method: oauth or bot_token | | `botToken` | string | No | Bot token for Custom Bot | -| `accessToken` | string | No | OAuth access token or bot token for Slack API | | `channel` | string | Yes | Target Slack channel \(e.g., #general\) | | `text` | string | Yes | Message text to send \(supports Slack mrkdwn formatting\) | @@ -89,10 +88,7 @@ Send messages to Slack channels or users through the Slack API. Supports Slack m | Parameter | Type | Description | | --------- | ---- | ----------- | | `ts` | string | Message timestamp | -| `channel` | string | Channel identifier | -| `canvas_id` | string | Canvas identifier | -| `title` | string | Canvas title | -| `messages` | json | Message data | +| `channel` | string | Channel ID where message was sent | ### `slack_canvas` @@ -104,7 +100,6 @@ Create and share Slack canvases in channels. Canvases are collaborative document | --------- | ---- | -------- | ----------- | | `authMethod` | string | No | Authentication method: oauth or bot_token | | `botToken` | string | No | Bot token for Custom Bot | -| `accessToken` | string | No | OAuth access token or bot token for Slack API | | `channel` | string | Yes | Target Slack channel \(e.g., #general\) | | `title` | string | Yes | Title of the canvas | | `content` | string | Yes | Canvas content in markdown format | @@ -114,11 +109,9 @@ Create and share Slack canvases in channels. Canvases are collaborative document | Parameter | Type | Description | | --------- | ---- | ----------- | -| `ts` | string | Message timestamp | -| `channel` | string | Channel identifier | -| `canvas_id` | string | Canvas identifier | -| `title` | string | Canvas title | -| `messages` | json | Message data | +| `canvas_id` | string | ID of the created canvas | +| `channel` | string | Channel where canvas was created | +| `title` | string | Title of the canvas | ### `slack_message_reader` @@ -130,7 +123,6 @@ Read the latest messages from Slack channels. Retrieve conversation history with | --------- | ---- | -------- | ----------- | | `authMethod` | string | No | Authentication method: oauth or bot_token | | `botToken` | string | No | Bot token for Custom Bot | -| `accessToken` | string | No | OAuth access token or bot token for Slack API | | `channel` | string | Yes | Slack channel to read messages from \(e.g., #general\) | | `limit` | number | No | Number of messages to retrieve \(default: 10, max: 100\) | | `oldest` | string | No | Start of time range \(timestamp\) | @@ -140,11 +132,7 @@ Read the latest messages from Slack channels. Retrieve conversation history with | Parameter | Type | Description | | --------- | ---- | ----------- | -| `ts` | string | Message timestamp | -| `channel` | string | Channel identifier | -| `canvas_id` | string | Canvas identifier | -| `title` | string | Canvas title | -| `messages` | json | Message data | +| `messages` | array | Array of message objects from the channel | diff --git a/apps/docs/content/docs/tools/stagehand.mdx b/apps/docs/content/docs/tools/stagehand.mdx index c0fe0e6ca6..eef1a2cb7d 100644 --- a/apps/docs/content/docs/tools/stagehand.mdx +++ b/apps/docs/content/docs/tools/stagehand.mdx @@ -214,7 +214,7 @@ Extract structured data from a webpage using Stagehand | Parameter | Type | Description | | --------- | ---- | ----------- | -| `data` | json | Extracted data | +| `data` | object | Extracted structured data matching the provided schema | diff --git a/apps/docs/content/docs/tools/stagehand_agent.mdx b/apps/docs/content/docs/tools/stagehand_agent.mdx index 3f1b5f20b5..fa48d02e6f 100644 --- a/apps/docs/content/docs/tools/stagehand_agent.mdx +++ b/apps/docs/content/docs/tools/stagehand_agent.mdx @@ -212,6 +212,7 @@ Run an autonomous web agent to complete tasks and extract structured data | `startUrl` | string | Yes | URL of the webpage to start the agent on | | `task` | string | Yes | The task to complete or goal to achieve on the website | | `variables` | json | No | Optional variables to substitute in the task \(format: \{key: value\}\). Reference in task using %key% | +| `format` | string | No | No description | | `apiKey` | string | Yes | OpenAI API key for agent execution \(required by Stagehand\) | | `outputSchema` | json | No | Optional JSON schema defining the structure of data the agent should return | @@ -219,8 +220,7 @@ Run an autonomous web agent to complete tasks and extract structured data | Parameter | Type | Description | | --------- | ---- | ----------- | -| `agentResult` | json | Agent execution result | -| `structuredOutput` | any | Structured output data | +| `agentResult` | object | Result from the Stagehand agent execution | diff --git a/apps/docs/content/docs/tools/supabase.mdx b/apps/docs/content/docs/tools/supabase.mdx index 772967fc25..c43f284246 100644 --- a/apps/docs/content/docs/tools/supabase.mdx +++ b/apps/docs/content/docs/tools/supabase.mdx @@ -92,7 +92,7 @@ Query data from a Supabase table | --------- | ---- | -------- | ----------- | | `projectId` | string | Yes | Your Supabase project ID \(e.g., jdrkgepadsdopsntdlom\) | | `table` | string | Yes | The name of the Supabase table to query | -| `filter` | string | No | PostgREST filter \(e.g., | +| `filter` | string | No | PostgREST filter \(e.g., "id=eq.123"\) | | `orderBy` | string | No | Column to order by \(add DESC for descending\) | | `limit` | number | No | Maximum number of rows to return | | `apiKey` | string | Yes | Your Supabase service role secret key | @@ -101,8 +101,8 @@ Query data from a Supabase table | Parameter | Type | Description | | --------- | ---- | ----------- | -| `message` | string | Operation message | -| `results` | json | Query results | +| `success` | boolean | Operation success status | +| `output` | object | Query operation results | ### `supabase_insert` @@ -121,8 +121,8 @@ Insert data into a Supabase table | Parameter | Type | Description | | --------- | ---- | ----------- | -| `message` | string | Operation message | -| `results` | json | Query results | +| `success` | boolean | Operation success status | +| `output` | object | Insert operation results | ### `supabase_get_row` @@ -134,15 +134,15 @@ Get a single row from a Supabase table based on filter criteria | --------- | ---- | -------- | ----------- | | `projectId` | string | Yes | Your Supabase project ID \(e.g., jdrkgepadsdopsntdlom\) | | `table` | string | Yes | The name of the Supabase table to query | -| `filter` | string | Yes | PostgREST filter to find the specific row \(e.g., | +| `filter` | string | Yes | PostgREST filter to find the specific row \(e.g., "id=eq.123"\) | | `apiKey` | string | Yes | Your Supabase service role secret key | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `message` | string | Operation message | -| `results` | json | Query results | +| `success` | boolean | Operation success status | +| `output` | object | Get row operation results | ### `supabase_update` @@ -154,7 +154,7 @@ Update rows in a Supabase table based on filter criteria | --------- | ---- | -------- | ----------- | | `projectId` | string | Yes | Your Supabase project ID \(e.g., jdrkgepadsdopsntdlom\) | | `table` | string | Yes | The name of the Supabase table to update | -| `filter` | string | Yes | PostgREST filter to identify rows to update \(e.g., | +| `filter` | string | Yes | PostgREST filter to identify rows to update \(e.g., "id=eq.123"\) | | `data` | object | Yes | Data to update in the matching rows | | `apiKey` | string | Yes | Your Supabase service role secret key | @@ -162,8 +162,8 @@ Update rows in a Supabase table based on filter criteria | Parameter | Type | Description | | --------- | ---- | ----------- | -| `message` | string | Operation message | -| `results` | json | Query results | +| `success` | boolean | Operation success status | +| `output` | object | Update operation results | ### `supabase_delete` @@ -175,15 +175,15 @@ Delete rows from a Supabase table based on filter criteria | --------- | ---- | -------- | ----------- | | `projectId` | string | Yes | Your Supabase project ID \(e.g., jdrkgepadsdopsntdlom\) | | `table` | string | Yes | The name of the Supabase table to delete from | -| `filter` | string | Yes | PostgREST filter to identify rows to delete \(e.g., | +| `filter` | string | Yes | PostgREST filter to identify rows to delete \(e.g., "id=eq.123"\) | | `apiKey` | string | Yes | Your Supabase service role secret key | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `message` | string | Operation message | -| `results` | json | Query results | +| `success` | boolean | Operation success status | +| `output` | object | Delete operation results | diff --git a/apps/docs/content/docs/tools/tavily.mdx b/apps/docs/content/docs/tools/tavily.mdx index 3f4ec5b605..c54e49c2ff 100644 --- a/apps/docs/content/docs/tools/tavily.mdx +++ b/apps/docs/content/docs/tools/tavily.mdx @@ -80,12 +80,8 @@ Perform AI-powered web searches using Tavily | Parameter | Type | Description | | --------- | ---- | ----------- | -| `results` | json | Search results data | -| `answer` | any | Search answer | -| `query` | string | Query used | -| `content` | string | Extracted content | -| `title` | string | Page title | -| `url` | string | Source URL | +| `query` | string | The search query that was executed | +| `results` | array | results output from the tool | ### `tavily_extract` @@ -103,12 +99,7 @@ Extract raw content from multiple web pages simultaneously using Tavily | Parameter | Type | Description | | --------- | ---- | ----------- | -| `results` | json | Search results data | -| `answer` | any | Search answer | -| `query` | string | Query used | -| `content` | string | Extracted content | -| `title` | string | Page title | -| `url` | string | Source URL | +| `results` | array | The URL that was extracted | diff --git a/apps/docs/content/docs/tools/telegram.mdx b/apps/docs/content/docs/tools/telegram.mdx index 155d82edfe..2dd0beae2c 100644 --- a/apps/docs/content/docs/tools/telegram.mdx +++ b/apps/docs/content/docs/tools/telegram.mdx @@ -1,6 +1,6 @@ --- title: Telegram -description: Send a message through Telegram +description: Send messages through Telegram or trigger workflows from Telegram events --- import { BlockInfoCard } from "@/components/ui/block-info-card" @@ -67,7 +67,7 @@ In Sim, the Telegram integration enables your agents to leverage these powerful ## Usage Instructions -Send messages to any Telegram channel using your Bot API key. Integrate automated notifications and alerts into your workflow to keep your team informed. +Send messages to any Telegram channel using your Bot API key or trigger workflows from Telegram bot messages. Integrate automated notifications and alerts into your workflow to keep your team informed. @@ -89,8 +89,12 @@ Send messages to Telegram channels or users through the Telegram Bot API. Enable | Parameter | Type | Description | | --------- | ---- | ----------- | -| `ok` | boolean | Success status | -| `result` | json | Message result | +| `success` | boolean | Telegram message send success status | +| `messageId` | number | Unique Telegram message identifier | +| `chatId` | string | Target chat ID where message was sent | +| `text` | string | Text content of the sent message | +| `timestamp` | number | Unix timestamp when message was sent | +| `from` | object | Information about the bot that sent the message | diff --git a/apps/docs/content/docs/tools/thinking.mdx b/apps/docs/content/docs/tools/thinking.mdx index 917b8340b2..86d23e53b2 100644 --- a/apps/docs/content/docs/tools/thinking.mdx +++ b/apps/docs/content/docs/tools/thinking.mdx @@ -69,7 +69,7 @@ Processes a provided thought/instruction, making it available for subsequent ste | Parameter | Type | Description | | --------- | ---- | ----------- | -| `acknowledgedThought` | string | Acknowledged thought process | +| `acknowledgedThought` | string | The thought that was processed and acknowledged | diff --git a/apps/docs/content/docs/tools/translate.mdx b/apps/docs/content/docs/tools/translate.mdx index 27bfa26680..1676f1119d 100644 --- a/apps/docs/content/docs/tools/translate.mdx +++ b/apps/docs/content/docs/tools/translate.mdx @@ -67,7 +67,7 @@ Convert text between languages while preserving meaning, nuance, and formatting. | --------- | ---- | ----------- | | `content` | string | Translated text | | `model` | string | Model used | -| `tokens` | any | Token usage | +| `tokens` | json | Token usage | ### `anthropic_chat` @@ -85,7 +85,7 @@ Convert text between languages while preserving meaning, nuance, and formatting. | --------- | ---- | ----------- | | `content` | string | Translated text | | `model` | string | Model used | -| `tokens` | any | Token usage | +| `tokens` | json | Token usage | diff --git a/apps/docs/content/docs/tools/twilio_sms.mdx b/apps/docs/content/docs/tools/twilio_sms.mdx index dfbd529f26..25d4a5164e 100644 --- a/apps/docs/content/docs/tools/twilio_sms.mdx +++ b/apps/docs/content/docs/tools/twilio_sms.mdx @@ -58,10 +58,11 @@ Send text messages to single or multiple recipients using the Twilio API. | Parameter | Type | Description | | --------- | ---- | ----------- | -| `success` | boolean | Send success status | -| `messageId` | any | Message identifier | -| `status` | any | Delivery status | -| `error` | any | Error information | +| `success` | boolean | SMS send success status | +| `messageId` | string | Unique Twilio message identifier \(SID\) | +| `status` | string | Message delivery status from Twilio | +| `fromNumber` | string | Phone number message was sent from | +| `toNumber` | string | Phone number message was sent to | diff --git a/apps/docs/content/docs/tools/typeform.mdx b/apps/docs/content/docs/tools/typeform.mdx index 21550595b2..996490c660 100644 --- a/apps/docs/content/docs/tools/typeform.mdx +++ b/apps/docs/content/docs/tools/typeform.mdx @@ -95,9 +95,9 @@ Download files uploaded in Typeform responses | Parameter | Type | Description | | --------- | ---- | ----------- | -| `total_items` | number | Total response count | -| `page_count` | number | Total page count | -| `items` | json | Response items | +| `fileUrl` | string | Direct download URL for the uploaded file | +| `contentType` | string | MIME type of the uploaded file | +| `filename` | string | Original filename of the uploaded file | ### `typeform_insights` @@ -114,9 +114,7 @@ Retrieve insights and analytics for Typeform forms | Parameter | Type | Description | | --------- | ---- | ----------- | -| `total_items` | number | Total response count | -| `page_count` | number | Total page count | -| `items` | json | Response items | +| `fields` | array | Number of users who dropped off at this field | diff --git a/apps/docs/content/docs/tools/vision.mdx b/apps/docs/content/docs/tools/vision.mdx index cf1ea47ab2..c26c9937a5 100644 --- a/apps/docs/content/docs/tools/vision.mdx +++ b/apps/docs/content/docs/tools/vision.mdx @@ -70,9 +70,10 @@ Process and analyze images using advanced vision models. Capable of understandin | Parameter | Type | Description | | --------- | ---- | ----------- | -| `content` | string | Analysis result | -| `model` | any | Model used | -| `tokens` | any | Token usage | +| `content` | string | The analyzed content and description of the image | +| `model` | string | The vision model that was used for analysis | +| `tokens` | number | Total tokens used for the analysis | +| `usage` | object | Detailed token usage breakdown | diff --git a/apps/docs/content/docs/tools/wealthbox.mdx b/apps/docs/content/docs/tools/wealthbox.mdx index d8cc291c52..a623401b53 100644 --- a/apps/docs/content/docs/tools/wealthbox.mdx +++ b/apps/docs/content/docs/tools/wealthbox.mdx @@ -56,21 +56,14 @@ Read content from a Wealthbox note | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `accessToken` | string | Yes | The access token for the Wealthbox API | | `noteId` | string | No | The ID of the note to read | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `note` | any | Note data | -| `notes` | any | Notes list | -| `contact` | any | Contact data | -| `contacts` | any | Contacts list | -| `task` | any | Task data | -| `tasks` | any | Tasks list | -| `metadata` | json | Operation metadata | -| `success` | any | Success status | +| `success` | boolean | Operation success status | +| `output` | object | Note data and metadata | ### `wealthbox_write_note` @@ -80,7 +73,6 @@ Create or update a Wealthbox note | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `accessToken` | string | Yes | The access token for the Wealthbox API | | `content` | string | Yes | The main body of the note | | `contactId` | string | No | ID of contact to link to this note | @@ -88,14 +80,8 @@ Create or update a Wealthbox note | Parameter | Type | Description | | --------- | ---- | ----------- | -| `note` | any | Note data | -| `notes` | any | Notes list | -| `contact` | any | Contact data | -| `contacts` | any | Contacts list | -| `task` | any | Task data | -| `tasks` | any | Tasks list | -| `metadata` | json | Operation metadata | -| `success` | any | Success status | +| `success` | boolean | Operation success status | +| `output` | object | Created or updated note data and metadata | ### `wealthbox_read_contact` @@ -105,21 +91,14 @@ Read content from a Wealthbox contact | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `accessToken` | string | Yes | The access token for the Wealthbox API | | `contactId` | string | No | The ID of the contact to read | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `note` | any | Note data | -| `notes` | any | Notes list | -| `contact` | any | Contact data | -| `contacts` | any | Contacts list | -| `task` | any | Task data | -| `tasks` | any | Tasks list | -| `metadata` | json | Operation metadata | -| `success` | any | Success status | +| `success` | boolean | Operation success status | +| `output` | object | Contact data and metadata | ### `wealthbox_write_contact` @@ -129,7 +108,6 @@ Create a new Wealthbox contact | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `accessToken` | string | Yes | The access token for the Wealthbox API | | `firstName` | string | Yes | The first name of the contact | | `lastName` | string | Yes | The last name of the contact | | `emailAddress` | string | No | The email address of the contact | @@ -139,14 +117,8 @@ Create a new Wealthbox contact | Parameter | Type | Description | | --------- | ---- | ----------- | -| `note` | any | Note data | -| `notes` | any | Notes list | -| `contact` | any | Contact data | -| `contacts` | any | Contacts list | -| `task` | any | Task data | -| `tasks` | any | Tasks list | -| `metadata` | json | Operation metadata | -| `success` | any | Success status | +| `success` | boolean | Operation success status | +| `output` | object | Created or updated contact data and metadata | ### `wealthbox_read_task` @@ -156,21 +128,14 @@ Read content from a Wealthbox task | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `accessToken` | string | Yes | The access token for the Wealthbox API | | `taskId` | string | No | The ID of the task to read | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `note` | any | Note data | -| `notes` | any | Notes list | -| `contact` | any | Contact data | -| `contacts` | any | Contacts list | -| `task` | any | Task data | -| `tasks` | any | Tasks list | -| `metadata` | json | Operation metadata | -| `success` | any | Success status | +| `success` | boolean | Operation success status | +| `output` | object | Task data and metadata | ### `wealthbox_write_task` @@ -180,9 +145,8 @@ Create or update a Wealthbox task | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `accessToken` | string | Yes | The access token for the Wealthbox API | | `title` | string | Yes | The name/title of the task | -| `dueDate` | string | Yes | The due date and time of the task \(format: | +| `dueDate` | string | Yes | The due date and time of the task \(format: "YYYY-MM-DD HH:MM AM/PM -HHMM", e.g., "2015-05-24 11:00 AM -0400"\) | | `contactId` | string | No | ID of contact to link to this task | | `description` | string | No | Description or notes about the task | @@ -190,14 +154,8 @@ Create or update a Wealthbox task | Parameter | Type | Description | | --------- | ---- | ----------- | -| `note` | any | Note data | -| `notes` | any | Notes list | -| `contact` | any | Contact data | -| `contacts` | any | Contacts list | -| `task` | any | Task data | -| `tasks` | any | Tasks list | -| `metadata` | json | Operation metadata | -| `success` | any | Success status | +| `success` | boolean | Operation success status | +| `output` | object | Created or updated task data and metadata | diff --git a/apps/docs/content/docs/tools/whatsapp.mdx b/apps/docs/content/docs/tools/whatsapp.mdx index a8b3e96750..d8a30b6853 100644 --- a/apps/docs/content/docs/tools/whatsapp.mdx +++ b/apps/docs/content/docs/tools/whatsapp.mdx @@ -54,15 +54,16 @@ Send WhatsApp messages | `phoneNumber` | string | Yes | Recipient phone number with country code | | `message` | string | Yes | Message content to send | | `phoneNumberId` | string | Yes | WhatsApp Business Phone Number ID | -| `accessToken` | string | Yes | WhatsApp Business API Access Token | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `success` | boolean | Send success status | -| `messageId` | any | Message identifier | -| `error` | any | Error information | +| `success` | boolean | WhatsApp message send success status | +| `messageId` | string | Unique WhatsApp message identifier | +| `phoneNumber` | string | Recipient phone number | +| `status` | string | Message delivery status | +| `timestamp` | string | Message send timestamp | diff --git a/apps/docs/content/docs/tools/wikipedia.mdx b/apps/docs/content/docs/tools/wikipedia.mdx index 6556945e57..ece9053cfc 100644 --- a/apps/docs/content/docs/tools/wikipedia.mdx +++ b/apps/docs/content/docs/tools/wikipedia.mdx @@ -74,11 +74,7 @@ Get a summary and metadata for a specific Wikipedia page. | Parameter | Type | Description | | --------- | ---- | ----------- | -| `summary` | json | Page summary data | -| `searchResults` | json | Search results data | -| `totalHits` | number | Total search hits | -| `content` | json | Page content data | -| `randomPage` | json | Random page data | +| `summary` | object | Wikipedia page summary and metadata | ### `wikipedia_search` @@ -95,11 +91,7 @@ Search for Wikipedia pages by title or content. | Parameter | Type | Description | | --------- | ---- | ----------- | -| `summary` | json | Page summary data | -| `searchResults` | json | Search results data | -| `totalHits` | number | Total search hits | -| `content` | json | Page content data | -| `randomPage` | json | Random page data | +| `searchResults` | array | Array of matching Wikipedia pages | ### `wikipedia_content` @@ -115,11 +107,7 @@ Get the full HTML content of a Wikipedia page. | Parameter | Type | Description | | --------- | ---- | ----------- | -| `summary` | json | Page summary data | -| `searchResults` | json | Search results data | -| `totalHits` | number | Total search hits | -| `content` | json | Page content data | -| `randomPage` | json | Random page data | +| `content` | object | Full HTML content and metadata of the Wikipedia page | ### `wikipedia_random` @@ -134,11 +122,7 @@ Get a random Wikipedia page. | Parameter | Type | Description | | --------- | ---- | ----------- | -| `summary` | json | Page summary data | -| `searchResults` | json | Search results data | -| `totalHits` | number | Total search hits | -| `content` | json | Page content data | -| `randomPage` | json | Random page data | +| `randomPage` | object | Random Wikipedia page data | diff --git a/apps/docs/content/docs/tools/x.mdx b/apps/docs/content/docs/tools/x.mdx index e6c99d0451..c3abadbbf1 100644 --- a/apps/docs/content/docs/tools/x.mdx +++ b/apps/docs/content/docs/tools/x.mdx @@ -50,7 +50,6 @@ Post new tweets, reply to tweets, or create polls on X (Twitter) | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `accessToken` | string | Yes | X OAuth access token | | `text` | string | Yes | The text content of your tweet | | `replyTo` | string | No | ID of the tweet to reply to | | `mediaIds` | array | No | Array of media IDs to attach to the tweet | @@ -60,14 +59,7 @@ Post new tweets, reply to tweets, or create polls on X (Twitter) | Parameter | Type | Description | | --------- | ---- | ----------- | -| `tweet` | json | Tweet data | -| `replies` | any | Tweet replies | -| `context` | any | Tweet context | -| `tweets` | json | Tweets data | -| `includes` | any | Additional data | -| `meta` | json | Response metadata | -| `user` | json | User profile data | -| `recentTweets` | any | Recent tweets data | +| `tweet` | object | The newly created tweet data | ### `x_read` @@ -77,7 +69,6 @@ Read tweet details, including replies and conversation context | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `accessToken` | string | Yes | X OAuth access token | | `tweetId` | string | Yes | ID of the tweet to read | | `includeReplies` | boolean | No | Whether to include replies to the tweet | @@ -85,14 +76,7 @@ Read tweet details, including replies and conversation context | Parameter | Type | Description | | --------- | ---- | ----------- | -| `tweet` | json | Tweet data | -| `replies` | any | Tweet replies | -| `context` | any | Tweet context | -| `tweets` | json | Tweets data | -| `includes` | any | Additional data | -| `meta` | json | Response metadata | -| `user` | json | User profile data | -| `recentTweets` | any | Recent tweets data | +| `tweet` | object | The main tweet data | ### `x_search` @@ -102,7 +86,6 @@ Search for tweets using keywords, hashtags, or advanced queries | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `accessToken` | string | Yes | X OAuth access token | | `query` | string | Yes | Search query \(supports X search operators\) | | `maxResults` | number | No | Maximum number of results to return \(default: 10, max: 100\) | | `startTime` | string | No | Start time for search \(ISO 8601 format\) | @@ -113,14 +96,7 @@ Search for tweets using keywords, hashtags, or advanced queries | Parameter | Type | Description | | --------- | ---- | ----------- | -| `tweet` | json | Tweet data | -| `replies` | any | Tweet replies | -| `context` | any | Tweet context | -| `tweets` | json | Tweets data | -| `includes` | any | Additional data | -| `meta` | json | Response metadata | -| `user` | json | User profile data | -| `recentTweets` | any | Recent tweets data | +| `tweets` | array | Array of tweets matching the search query | ### `x_user` @@ -130,21 +106,13 @@ Get user profile information | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `accessToken` | string | Yes | X OAuth access token | | `username` | string | Yes | Username to look up \(without @ symbol\) | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `tweet` | json | Tweet data | -| `replies` | any | Tweet replies | -| `context` | any | Tweet context | -| `tweets` | json | Tweets data | -| `includes` | any | Additional data | -| `meta` | json | Response metadata | -| `user` | json | User profile data | -| `recentTweets` | any | Recent tweets data | +| `user` | object | X user profile information | diff --git a/apps/docs/content/docs/tools/youtube.mdx b/apps/docs/content/docs/tools/youtube.mdx index 6f8ddda348..b22c3b934f 100644 --- a/apps/docs/content/docs/tools/youtube.mdx +++ b/apps/docs/content/docs/tools/youtube.mdx @@ -62,8 +62,7 @@ Search for videos on YouTube using the YouTube Data API. | Parameter | Type | Description | | --------- | ---- | ----------- | -| `items` | json | The items returned by the YouTube search | -| `totalResults` | number | The total number of results returned by the YouTube search | +| `items` | array | Array of YouTube videos matching the search query | diff --git a/apps/docs/content/docs/triggers/starter.mdx b/apps/docs/content/docs/triggers/starter.mdx index 345a1e3a8f..a156843086 100644 --- a/apps/docs/content/docs/triggers/starter.mdx +++ b/apps/docs/content/docs/triggers/starter.mdx @@ -50,7 +50,7 @@ Choose your input method from the dropdown: -

Chat with your workflow and access both input text and conversation ID for context-aware responses.

+

Chat with your workflow and access input text, conversation ID, and uploaded files for context-aware responses.

@@ -60,13 +60,15 @@ Choose your input method from the dropdown: In Chat mode, access user input and conversation context through special variables: ```yaml -# Reference the chat input and conversation ID in your workflow +# Reference the chat input, conversation ID, and files in your workflow user_message: "" conversation_id: "" +uploaded_files: "" ``` - **``** - Contains the user's message text - **``** - Unique identifier for the conversation thread +- **``** - Array of files uploaded by the user (if any) ## API Execution diff --git a/apps/sim/app/(auth)/login/login-form.test.tsx b/apps/sim/app/(auth)/login/login-form.test.tsx index 6ab8cf5c96..8f6fa4af96 100644 --- a/apps/sim/app/(auth)/login/login-form.test.tsx +++ b/apps/sim/app/(auth)/login/login-form.test.tsx @@ -94,10 +94,10 @@ describe('LoginPage', () => { const emailInput = screen.getByPlaceholderText(/enter your email/i) const passwordInput = screen.getByPlaceholderText(/enter your password/i) - fireEvent.change(emailInput, { target: { value: 'test@example.com' } }) + fireEvent.change(emailInput, { target: { value: 'user@company.com' } }) fireEvent.change(passwordInput, { target: { value: 'password123' } }) - expect(emailInput).toHaveValue('test@example.com') + expect(emailInput).toHaveValue('user@company.com') expect(passwordInput).toHaveValue('password123') }) @@ -117,7 +117,7 @@ describe('LoginPage', () => { const submitButton = screen.getByRole('button', { name: /sign in/i }) await act(async () => { - fireEvent.change(emailInput, { target: { value: 'test@example.com' } }) + fireEvent.change(emailInput, { target: { value: 'user@company.com' } }) fireEvent.change(passwordInput, { target: { value: 'password123' } }) fireEvent.click(submitButton) }) @@ -140,14 +140,14 @@ describe('LoginPage', () => { const passwordInput = screen.getByPlaceholderText(/enter your password/i) const submitButton = screen.getByRole('button', { name: /sign in/i }) - fireEvent.change(emailInput, { target: { value: 'test@example.com' } }) + fireEvent.change(emailInput, { target: { value: 'user@company.com' } }) fireEvent.change(passwordInput, { target: { value: 'password123' } }) fireEvent.click(submitButton) await waitFor(() => { expect(mockSignIn).toHaveBeenCalledWith( { - email: 'test@example.com', + email: 'user@company.com', password: 'password123', callbackURL: '/workspace', }, @@ -181,7 +181,7 @@ describe('LoginPage', () => { const passwordInput = screen.getByPlaceholderText(/enter your password/i) const submitButton = screen.getByRole('button', { name: /sign in/i }) - fireEvent.change(emailInput, { target: { value: 'test@example.com' } }) + fireEvent.change(emailInput, { target: { value: 'user@company.com' } }) fireEvent.change(passwordInput, { target: { value: 'wrongpassword' } }) fireEvent.click(submitButton) @@ -242,13 +242,13 @@ describe('LoginPage', () => { const passwordInput = screen.getByPlaceholderText(/enter your password/i) const submitButton = screen.getByRole('button', { name: /sign in/i }) - fireEvent.change(emailInput, { target: { value: 'test@example.com' } }) + fireEvent.change(emailInput, { target: { value: 'user@company.com' } }) fireEvent.change(passwordInput, { target: { value: 'password123' } }) fireEvent.click(submitButton) await waitFor(() => { expect(mockSendOtp).toHaveBeenCalledWith({ - email: 'test@example.com', + email: 'user@company.com', type: 'email-verification', }) expect(mockRouter.push).toHaveBeenCalledWith('/verify') diff --git a/apps/sim/app/(auth)/login/login-form.tsx b/apps/sim/app/(auth)/login/login-form.tsx index f6ca4aa458..6fcaadf63c 100644 --- a/apps/sim/app/(auth)/login/login-form.tsx +++ b/apps/sim/app/(auth)/login/login-form.tsx @@ -15,25 +15,27 @@ import { import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { client } from '@/lib/auth-client' +import { quickValidateEmail } from '@/lib/email/validation' import { createLogger } from '@/lib/logs/console/logger' import { cn } from '@/lib/utils' import { SocialLoginButtons } from '@/app/(auth)/components/social-login-buttons' const logger = createLogger('LoginForm') -const EMAIL_VALIDATIONS = { - required: { - test: (value: string) => Boolean(value && typeof value === 'string'), - message: 'Email is required.', - }, - notEmpty: { - test: (value: string) => value.trim().length > 0, - message: 'Email cannot be empty.', - }, - basicFormat: { - regex: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, - message: 'Please enter a valid email address.', - }, +const validateEmailField = (emailValue: string): string[] => { + const errors: string[] = [] + + if (!emailValue || !emailValue.trim()) { + errors.push('Email is required.') + return errors + } + + const validation = quickValidateEmail(emailValue.trim().toLowerCase()) + if (!validation.isValid) { + errors.push(validation.reason || 'Please enter a valid email address.') + } + + return errors } const PASSWORD_VALIDATIONS = { @@ -68,27 +70,6 @@ const validateCallbackUrl = (url: string): boolean => { } } -// Validate email and return array of error messages -const validateEmail = (emailValue: string): string[] => { - const errors: string[] = [] - - if (!EMAIL_VALIDATIONS.required.test(emailValue)) { - errors.push(EMAIL_VALIDATIONS.required.message) - return errors // Return early for required field - } - - if (!EMAIL_VALIDATIONS.notEmpty.test(emailValue)) { - errors.push(EMAIL_VALIDATIONS.notEmpty.message) - return errors // Return early for empty field - } - - if (!EMAIL_VALIDATIONS.basicFormat.regex.test(emailValue)) { - errors.push(EMAIL_VALIDATIONS.basicFormat.message) - } - - return errors -} - // Validate password and return array of error messages const validatePassword = (passwordValue: string): string[] => { const errors: string[] = [] @@ -182,7 +163,7 @@ export default function LoginPage({ setEmail(newEmail) // Silently validate but don't show errors until submit - const errors = validateEmail(newEmail) + const errors = validateEmailField(newEmail) setEmailErrors(errors) setShowEmailValidationError(false) } @@ -205,7 +186,7 @@ export default function LoginPage({ const email = formData.get('email') as string // Validate email on submit - const emailValidationErrors = validateEmail(email) + const emailValidationErrors = validateEmailField(email) setEmailErrors(emailValidationErrors) setShowEmailValidationError(emailValidationErrors.length > 0) diff --git a/apps/sim/app/(auth)/signup/signup-form.test.tsx b/apps/sim/app/(auth)/signup/signup-form.test.tsx index adb4b025fb..014acba8fe 100644 --- a/apps/sim/app/(auth)/signup/signup-form.test.tsx +++ b/apps/sim/app/(auth)/signup/signup-form.test.tsx @@ -96,11 +96,11 @@ describe('SignupPage', () => { const passwordInput = screen.getByPlaceholderText(/enter your password/i) fireEvent.change(nameInput, { target: { value: 'John Doe' } }) - fireEvent.change(emailInput, { target: { value: 'test@example.com' } }) + fireEvent.change(emailInput, { target: { value: 'user@company.com' } }) fireEvent.change(passwordInput, { target: { value: 'Password123!' } }) expect(nameInput).toHaveValue('John Doe') - expect(emailInput).toHaveValue('test@example.com') + expect(emailInput).toHaveValue('user@company.com') expect(passwordInput).toHaveValue('Password123!') }) @@ -118,7 +118,7 @@ describe('SignupPage', () => { const submitButton = screen.getByRole('button', { name: /create account/i }) fireEvent.change(nameInput, { target: { value: 'John Doe' } }) - fireEvent.change(emailInput, { target: { value: 'test@example.com' } }) + fireEvent.change(emailInput, { target: { value: 'user@company.com' } }) fireEvent.change(passwordInput, { target: { value: 'Password123!' } }) fireEvent.click(submitButton) @@ -144,14 +144,14 @@ describe('SignupPage', () => { // Use valid input that passes all validation rules fireEvent.change(nameInput, { target: { value: 'John Doe' } }) - fireEvent.change(emailInput, { target: { value: 'test@example.com' } }) + fireEvent.change(emailInput, { target: { value: 'user@company.com' } }) fireEvent.change(passwordInput, { target: { value: 'Password123!' } }) fireEvent.click(submitButton) await waitFor(() => { expect(mockSignUp).toHaveBeenCalledWith( { - email: 'test@example.com', + email: 'user@company.com', password: 'Password123!', name: 'John Doe', }, @@ -174,7 +174,7 @@ describe('SignupPage', () => { // Use name with leading/trailing spaces which should fail validation fireEvent.change(nameInput, { target: { value: ' John Doe ' } }) - fireEvent.change(emailInput, { target: { value: 'test@example.com' } }) + fireEvent.change(emailInput, { target: { value: 'user@company.com' } }) fireEvent.change(passwordInput, { target: { value: 'Password123!' } }) fireEvent.click(submitButton) @@ -206,15 +206,13 @@ describe('SignupPage', () => { const submitButton = screen.getByRole('button', { name: /create account/i }) fireEvent.change(nameInput, { target: { value: 'John Doe' } }) - fireEvent.change(emailInput, { target: { value: 'test@example.com' } }) + fireEvent.change(emailInput, { target: { value: 'user@company.com' } }) fireEvent.change(passwordInput, { target: { value: 'Password123!' } }) fireEvent.click(submitButton) await waitFor(() => { - expect(mockSendOtp).toHaveBeenCalledWith({ - email: 'test@example.com', - type: 'email-verification', - }) + // With sendVerificationOnSignUp: true, OTP is sent automatically by Better Auth + // No manual OTP sending in the component anymore expect(mockRouter.push).toHaveBeenCalledWith('/verify?fromSignup=true') }) }) @@ -267,7 +265,7 @@ describe('SignupPage', () => { const submitButton = screen.getByRole('button', { name: /create account/i }) fireEvent.change(nameInput, { target: { value: longName } }) - fireEvent.change(emailInput, { target: { value: 'test@example.com' } }) + fireEvent.change(emailInput, { target: { value: 'user@company.com' } }) fireEvent.change(passwordInput, { target: { value: 'ValidPass123!' } }) fireEvent.click(submitButton) @@ -295,7 +293,7 @@ describe('SignupPage', () => { const submitButton = screen.getByRole('button', { name: /create account/i }) fireEvent.change(nameInput, { target: { value: exactLengthName } }) - fireEvent.change(emailInput, { target: { value: 'test@example.com' } }) + fireEvent.change(emailInput, { target: { value: 'user@company.com' } }) fireEvent.change(passwordInput, { target: { value: 'ValidPass123!' } }) fireEvent.click(submitButton) @@ -308,7 +306,7 @@ describe('SignupPage', () => { await waitFor(() => { expect(mockSignUp).toHaveBeenCalledWith( { - email: 'test@example.com', + email: 'user@company.com', password: 'ValidPass123!', name: exactLengthName, }, @@ -343,7 +341,7 @@ describe('SignupPage', () => { await act(async () => { fireEvent.change(nameInput, { target: { value: 'John Doe' } }) - fireEvent.change(emailInput, { target: { value: 'test@example.com' } }) + fireEvent.change(emailInput, { target: { value: 'user@company.com' } }) fireEvent.change(passwordInput, { target: { value: 'Password123!' } }) fireEvent.click(submitButton) }) @@ -385,12 +383,12 @@ describe('SignupPage', () => { const submitButton = screen.getByRole('button', { name: /create account/i }) fireEvent.change(nameInput, { target: { value: 'John Doe' } }) - fireEvent.change(emailInput, { target: { value: 'test@example.com' } }) + fireEvent.change(emailInput, { target: { value: 'user@company.com' } }) fireEvent.change(passwordInput, { target: { value: 'Password123!' } }) fireEvent.click(submitButton) await waitFor(() => { - expect(mockRouter.push).toHaveBeenCalledWith('/invite/123') + expect(mockRouter.push).toHaveBeenCalledWith('/verify?fromSignup=true') }) }) diff --git a/apps/sim/app/(auth)/signup/signup-form.tsx b/apps/sim/app/(auth)/signup/signup-form.tsx index 8b5e88dc81..287c62bf77 100644 --- a/apps/sim/app/(auth)/signup/signup-form.tsx +++ b/apps/sim/app/(auth)/signup/signup-form.tsx @@ -8,9 +8,13 @@ import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { client } from '@/lib/auth-client' +import { quickValidateEmail } from '@/lib/email/validation' +import { createLogger } from '@/lib/logs/console/logger' import { cn } from '@/lib/utils' import { SocialLoginButtons } from '@/app/(auth)/components/social-login-buttons' +const logger = createLogger('SignupForm') + const PASSWORD_VALIDATIONS = { minLength: { regex: /.{8,}/, message: 'Password must be at least 8 characters long.' }, uppercase: { @@ -51,31 +55,20 @@ const NAME_VALIDATIONS = { }, } -const EMAIL_VALIDATIONS = { - required: { - test: (value: string) => Boolean(value && typeof value === 'string'), - message: 'Email is required.', - }, - notEmpty: { - test: (value: string) => value.trim().length > 0, - message: 'Email cannot be empty.', - }, - maxLength: { - test: (value: string) => value.length <= 254, - message: 'Email must be less than 254 characters.', - }, - basicFormat: { - regex: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, - message: 'Please enter a valid email address.', - }, - noSpaces: { - regex: /^[^\s]*$/, - message: 'Email cannot contain spaces.', - }, - validStart: { - regex: /^[a-zA-Z0-9]/, - message: 'Email must start with a letter or number.', - }, +const validateEmailField = (emailValue: string): string[] => { + const errors: string[] = [] + + if (!emailValue || !emailValue.trim()) { + errors.push('Email is required.') + return errors + } + + const validation = quickValidateEmail(emailValue.trim().toLowerCase()) + if (!validation.isValid) { + errors.push(validation.reason || 'Please enter a valid email address.') + } + + return errors } function SignupFormContent({ @@ -188,39 +181,6 @@ function SignupFormContent({ return errors } - // Validate email and return array of error messages - const validateEmail = (emailValue: string): string[] => { - const errors: string[] = [] - - if (!EMAIL_VALIDATIONS.required.test(emailValue)) { - errors.push(EMAIL_VALIDATIONS.required.message) - return errors // Return early for required field - } - - if (!EMAIL_VALIDATIONS.notEmpty.test(emailValue)) { - errors.push(EMAIL_VALIDATIONS.notEmpty.message) - return errors // Return early for empty field - } - - if (!EMAIL_VALIDATIONS.maxLength.test(emailValue)) { - errors.push(EMAIL_VALIDATIONS.maxLength.message) - } - - if (!EMAIL_VALIDATIONS.noSpaces.regex.test(emailValue)) { - errors.push(EMAIL_VALIDATIONS.noSpaces.message) - } - - if (!EMAIL_VALIDATIONS.validStart.regex.test(emailValue)) { - errors.push(EMAIL_VALIDATIONS.validStart.message) - } - - if (!EMAIL_VALIDATIONS.basicFormat.regex.test(emailValue)) { - errors.push(EMAIL_VALIDATIONS.basicFormat.message) - } - - return errors - } - const handlePasswordChange = (e: React.ChangeEvent) => { const newPassword = e.target.value setPassword(newPassword) @@ -246,7 +206,7 @@ function SignupFormContent({ setEmail(newEmail) // Silently validate but don't show errors until submit - const errors = validateEmail(newEmail) + const errors = validateEmailField(newEmail) setEmailErrors(errors) setShowEmailValidationError(false) @@ -271,7 +231,7 @@ function SignupFormContent({ setShowNameValidationError(nameValidationErrors.length > 0) // Validate email on submit - const emailValidationErrors = validateEmail(emailValue) + const emailValidationErrors = validateEmailField(emailValue) setEmailErrors(emailValidationErrors) setShowEmailValidationError(emailValidationErrors.length > 0) @@ -324,7 +284,7 @@ function SignupFormContent({ }, { onError: (ctx) => { - console.error('Signup error:', ctx.error) + logger.error('Signup error:', ctx.error) const errorMessage: string[] = ['Failed to create account'] if (ctx.error.code?.includes('USER_ALREADY_EXISTS')) { @@ -370,30 +330,37 @@ function SignupFormContent({ return } - // Handle invitation flow redirect - if (isInviteFlow && redirectUrl) { - router.push(redirectUrl) - return + // For new signups, always require verification + if (typeof window !== 'undefined') { + sessionStorage.setItem('verificationEmail', emailValue) + localStorage.setItem('has_logged_in_before', 'true') + + // Set cookie flag for middleware check + document.cookie = 'requiresEmailVerification=true; path=/; max-age=900; SameSite=Lax' // 15 min expiry + document.cookie = 'has_logged_in_before=true; path=/; max-age=31536000; SameSite=Lax' + + // Store invitation flow state if applicable + if (isInviteFlow && redirectUrl) { + sessionStorage.setItem('inviteRedirectUrl', redirectUrl) + sessionStorage.setItem('isInviteFlow', 'true') + } } + // Send verification OTP manually try { await client.emailOtp.sendVerificationOtp({ email: emailValue, type: 'email-verification', }) - } catch (err) { - console.error('Failed to send verification OTP:', err) - } - - if (typeof window !== 'undefined') { - sessionStorage.setItem('verificationEmail', emailValue) - localStorage.setItem('has_logged_in_before', 'true') - document.cookie = 'has_logged_in_before=true; path=/; max-age=31536000; SameSite=Lax' // 1 year expiry + } catch (otpError) { + logger.error('Failed to send OTP:', otpError) + // Continue anyway - user can use resend button } + // Always redirect to verification for new signups router.push('/verify?fromSignup=true') } catch (error) { - console.error('Signup error:', error) + logger.error('Signup error:', error) setIsLoading(false) } } diff --git a/apps/sim/app/(auth)/verify/use-verification.ts b/apps/sim/app/(auth)/verify/use-verification.ts index 549fd675e2..139ffbcc32 100644 --- a/apps/sim/app/(auth)/verify/use-verification.ts +++ b/apps/sim/app/(auth)/verify/use-verification.ts @@ -121,10 +121,14 @@ export function useVerification({ if (response && !response.error) { setIsVerified(true) - // Clear email from sessionStorage after successful verification + // Clear verification requirements and session storage if (typeof window !== 'undefined') { sessionStorage.removeItem('verificationEmail') + // Clear the verification requirement flag + document.cookie = + 'requiresEmailVerification=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT' + // Also clear invite-related items if (isInviteFlow) { sessionStorage.removeItem('inviteRedirectUrl') @@ -223,6 +227,11 @@ export function useVerification({ // Auto-verify and redirect in development/docker environments if (isDevOrDocker || !hasResendKey) { setIsVerified(true) + + // Clear verification requirement cookie (same as manual verification) + document.cookie = + 'requiresEmailVerification=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT' + const timeoutId = setTimeout(() => { router.push('/workspace') }, 1000) diff --git a/apps/sim/app/api/__test-utils__/utils.ts b/apps/sim/app/api/__test-utils__/utils.ts index 9918d44e0b..9f5cdf21c5 100644 --- a/apps/sim/app/api/__test-utils__/utils.ts +++ b/apps/sim/app/api/__test-utils__/utils.ts @@ -99,6 +99,7 @@ export const sampleWorkflowState = { horizontalHandles: true, isWide: false, advancedMode: false, + triggerMode: false, height: 95, }, 'agent-id': { @@ -127,6 +128,7 @@ export const sampleWorkflowState = { horizontalHandles: true, isWide: false, advancedMode: false, + triggerMode: false, height: 680, }, }, @@ -712,6 +714,7 @@ export function mockFileSystem( } return Promise.reject(new Error('File not found')) }), + mkdir: vi.fn().mockResolvedValue(undefined), })) } @@ -761,14 +764,15 @@ export function createStorageProviderMocks(options: StorageProviderMockOptions = getStorageProvider: vi.fn().mockReturnValue(provider), isUsingCloudStorage: vi.fn().mockReturnValue(isCloudEnabled), uploadFile: vi.fn().mockResolvedValue({ - path: '/api/files/serve/test-key', - key: 'test-key', + path: '/api/files/serve/test-key.txt', + key: 'test-key.txt', name: 'test.txt', size: 100, type: 'text/plain', }), downloadFile: vi.fn().mockResolvedValue(Buffer.from('test content')), deleteFile: vi.fn().mockResolvedValue(undefined), + getPresignedUrl: vi.fn().mockResolvedValue(presignedUrl), })) if (provider === 's3') { @@ -1235,14 +1239,15 @@ export function setupFileApiMocks( getStorageProvider: vi.fn().mockReturnValue('local'), isUsingCloudStorage: vi.fn().mockReturnValue(cloudEnabled), uploadFile: vi.fn().mockResolvedValue({ - path: '/api/files/serve/test-key', - key: 'test-key', + path: '/api/files/serve/test-key.txt', + key: 'test-key.txt', name: 'test.txt', size: 100, type: 'text/plain', }), downloadFile: vi.fn().mockResolvedValue(Buffer.from('test content')), deleteFile: vi.fn().mockResolvedValue(undefined), + getPresignedUrl: vi.fn().mockResolvedValue('https://example.com/presigned-url'), })) } @@ -1347,8 +1352,8 @@ export function mockUploadUtils( const { isCloudStorage = false, uploadResult = { - path: '/api/files/serve/test-key', - key: 'test-key', + path: '/api/files/serve/test-key.txt', + key: 'test-key.txt', name: 'test.txt', size: 100, type: 'text/plain', diff --git a/apps/sim/app/api/files/download/route.ts b/apps/sim/app/api/files/download/route.ts new file mode 100644 index 0000000000..d1429b1289 --- /dev/null +++ b/apps/sim/app/api/files/download/route.ts @@ -0,0 +1,99 @@ +import { type NextRequest, NextResponse } from 'next/server' +import { createLogger } from '@/lib/logs/console/logger' +import { getPresignedUrl, getPresignedUrlWithConfig, isUsingCloudStorage } from '@/lib/uploads' +import { BLOB_EXECUTION_FILES_CONFIG, S3_EXECUTION_FILES_CONFIG } from '@/lib/uploads/setup' +import { createErrorResponse } from '@/app/api/files/utils' + +const logger = createLogger('FileDownload') + +export const dynamic = 'force-dynamic' + +export async function POST(request: NextRequest) { + try { + const body = await request.json() + const { key, name, storageProvider, bucketName, isExecutionFile } = body + + if (!key) { + return createErrorResponse(new Error('File key is required'), 400) + } + + logger.info(`Generating download URL for file: ${name || key}`) + + if (isUsingCloudStorage()) { + // Generate a fresh 5-minute presigned URL for cloud storage + try { + let downloadUrl: string + + // Use execution files storage if flagged as execution file + if (isExecutionFile) { + logger.info(`Using execution files storage for file: ${key}`) + downloadUrl = await getPresignedUrlWithConfig( + key, + { + bucket: S3_EXECUTION_FILES_CONFIG.bucket, + region: S3_EXECUTION_FILES_CONFIG.region, + }, + 5 * 60 // 5 minutes + ) + } else if (storageProvider && (storageProvider === 's3' || storageProvider === 'blob')) { + // Use explicitly specified storage provider (legacy support) + logger.info(`Using specified storage provider '${storageProvider}' for file: ${key}`) + + if (storageProvider === 's3') { + downloadUrl = await getPresignedUrlWithConfig( + key, + { + bucket: bucketName || S3_EXECUTION_FILES_CONFIG.bucket, + region: S3_EXECUTION_FILES_CONFIG.region, + }, + 5 * 60 // 5 minutes + ) + } else { + // blob + downloadUrl = await getPresignedUrlWithConfig( + key, + { + accountName: BLOB_EXECUTION_FILES_CONFIG.accountName, + accountKey: BLOB_EXECUTION_FILES_CONFIG.accountKey, + connectionString: BLOB_EXECUTION_FILES_CONFIG.connectionString, + containerName: bucketName || BLOB_EXECUTION_FILES_CONFIG.containerName, + }, + 5 * 60 // 5 minutes + ) + } + } else { + // Use default storage (regular uploads) + logger.info(`Using default storage for file: ${key}`) + downloadUrl = await getPresignedUrl(key, 5 * 60) // 5 minutes + } + + return NextResponse.json({ + downloadUrl, + expiresIn: 300, // 5 minutes in seconds + fileName: name || key.split('/').pop() || 'download', + }) + } catch (error) { + logger.error(`Failed to generate presigned URL for ${key}:`, error) + return createErrorResponse( + error instanceof Error ? error : new Error('Failed to generate download URL'), + 500 + ) + } + } else { + // For local storage, return the direct path + const downloadUrl = `${process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'}/api/files/serve/${key}` + + return NextResponse.json({ + downloadUrl, + expiresIn: null, // Local URLs don't expire + fileName: name || key.split('/').pop() || 'download', + }) + } + } catch (error) { + logger.error('Error in file download endpoint:', error) + return createErrorResponse( + error instanceof Error ? error : new Error('Internal server error'), + 500 + ) + } +} diff --git a/apps/sim/app/api/files/execution/[executionId]/[fileId]/route.ts b/apps/sim/app/api/files/execution/[executionId]/[fileId]/route.ts new file mode 100644 index 0000000000..46b26f89ab --- /dev/null +++ b/apps/sim/app/api/files/execution/[executionId]/[fileId]/route.ts @@ -0,0 +1,70 @@ +import { type NextRequest, NextResponse } from 'next/server' +import { createLogger } from '@/lib/logs/console/logger' +import { generateExecutionFileDownloadUrl } from '@/lib/workflows/execution-file-storage' +import { getExecutionFiles } from '@/lib/workflows/execution-files-server' +import type { UserFile } from '@/executor/types' + +const logger = createLogger('ExecutionFileDownloadAPI') + +/** + * Generate a short-lived presigned URL for secure execution file download + * GET /api/files/execution/[executionId]/[fileId] + */ +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ executionId: string; fileId: string }> } +) { + try { + const { executionId, fileId } = await params + + if (!executionId || !fileId) { + return NextResponse.json({ error: 'Execution ID and File ID are required' }, { status: 400 }) + } + + logger.info(`Generating download URL for file ${fileId} in execution ${executionId}`) + + // Get files for this execution + const executionFiles = await getExecutionFiles(executionId) + + if (executionFiles.length === 0) { + return NextResponse.json({ error: 'No files found for this execution' }, { status: 404 }) + } + + // Find the specific file + const file = executionFiles.find((f) => f.id === fileId) + if (!file) { + return NextResponse.json({ error: 'File not found in this execution' }, { status: 404 }) + } + + // Check if file is expired + if (new Date(file.expiresAt) < new Date()) { + return NextResponse.json({ error: 'File has expired' }, { status: 410 }) + } + + // Since ExecutionFileMetadata is now just UserFile, no conversion needed + const userFile: UserFile = file + + // Generate a new short-lived presigned URL (5 minutes) + const downloadUrl = await generateExecutionFileDownloadUrl(userFile) + + logger.info(`Generated download URL for file ${file.name} (execution: ${executionId})`) + + const response = NextResponse.json({ + downloadUrl, + fileName: file.name, + fileSize: file.size, + fileType: file.type, + expiresIn: 300, // 5 minutes + }) + + // Ensure no caching of download URLs + response.headers.set('Cache-Control', 'no-cache, no-store, must-revalidate') + response.headers.set('Pragma', 'no-cache') + response.headers.set('Expires', '0') + + return response + } catch (error) { + logger.error('Error generating execution file download URL:', error) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + } +} diff --git a/apps/sim/app/api/files/parse/route.ts b/apps/sim/app/api/files/parse/route.ts index e7a9468380..763f688c07 100644 --- a/apps/sim/app/api/files/parse/route.ts +++ b/apps/sim/app/api/files/parse/route.ts @@ -7,7 +7,7 @@ import { type NextRequest, NextResponse } from 'next/server' import { isSupportedFileType, parseFile } from '@/lib/file-parsers' import { createLogger } from '@/lib/logs/console/logger' import { downloadFile, isUsingCloudStorage } from '@/lib/uploads' -import { UPLOAD_DIR } from '@/lib/uploads/setup' +import { UPLOAD_DIR_SERVER } from '@/lib/uploads/setup.server' import '@/lib/uploads/setup.server' export const dynamic = 'force-dynamic' @@ -70,7 +70,7 @@ export async function POST(request: NextRequest) { const requestData = await request.json() const { filePath, fileType } = requestData - if (!filePath) { + if (!filePath || (typeof filePath === 'string' && filePath.trim() === '')) { return NextResponse.json({ success: false, error: 'No file path provided' }, { status: 400 }) } @@ -80,6 +80,16 @@ export async function POST(request: NextRequest) { if (Array.isArray(filePath)) { const results = [] for (const path of filePath) { + // Skip empty or invalid paths + if (!path || (typeof path === 'string' && path.trim() === '')) { + results.push({ + success: false, + error: 'Empty file path in array', + filePath: path || '', + }) + continue + } + const result = await parseFileSingle(path, fileType) // Add processing time to metadata if (result.metadata) { @@ -154,6 +164,15 @@ export async function POST(request: NextRequest) { async function parseFileSingle(filePath: string, fileType?: string): Promise { logger.info('Parsing file:', filePath) + // Validate that filePath is not empty + if (!filePath || filePath.trim() === '') { + return { + success: false, + error: 'Empty file path provided', + filePath: filePath || '', + } + } + // Validate path for security before any processing const pathValidation = validateFilePath(filePath) if (!pathValidation.isValid) { @@ -337,7 +356,7 @@ async function handleLocalFile(filePath: string, fileType?: string): Promise = { @@ -194,6 +194,7 @@ async function handleS3PresignedUrl( let presignedUrl: string try { + const { getS3Client } = await import('@/lib/uploads/s3/s3-client') presignedUrl = await getSignedUrl(getS3Client(), command, { expiresIn: 3600 }) } catch (s3Error) { logger.error('Failed to generate S3 presigned URL:', s3Error) @@ -272,6 +273,7 @@ async function handleBlobPresignedUrl( const uniqueKey = `${prefix}${uuidv4()}-${safeFileName}` + const { getBlobServiceClient } = await import('@/lib/uploads/blob/blob-client') const blobServiceClient = getBlobServiceClient() const containerClient = blobServiceClient.getContainerClient(config.containerName) const blockBlobClient = containerClient.getBlockBlobClient(uniqueKey) diff --git a/apps/sim/app/api/files/upload/route.test.ts b/apps/sim/app/api/files/upload/route.test.ts index ec06804c4c..3069ad5cfc 100644 --- a/apps/sim/app/api/files/upload/route.test.ts +++ b/apps/sim/app/api/files/upload/route.test.ts @@ -26,7 +26,9 @@ describe('File Upload API Route', () => { beforeEach(() => { vi.resetModules() - vi.doMock('@/lib/uploads/setup.server', () => ({})) + vi.doMock('@/lib/uploads/setup.server', () => ({ + UPLOAD_DIR_SERVER: '/tmp/test-uploads', + })) }) afterEach(() => { @@ -52,6 +54,12 @@ describe('File Upload API Route', () => { const response = await POST(req) const data = await response.json() + // Log error details if test fails + if (response.status !== 200) { + console.error('Upload failed with status:', response.status) + console.error('Error response:', data) + } + expect(response.status).toBe(200) expect(data).toHaveProperty('path') expect(data.path).toMatch(/\/api\/files\/serve\/.*\.txt$/) @@ -59,8 +67,9 @@ describe('File Upload API Route', () => { expect(data).toHaveProperty('size') expect(data).toHaveProperty('type', 'text/plain') - const fs = await import('fs/promises') - expect(fs.writeFile).toHaveBeenCalled() + // Verify the upload function was called (we're mocking at the uploadFile level) + const { uploadFile } = await import('@/lib/uploads') + expect(uploadFile).toHaveBeenCalled() }) it('should upload a file to S3 when in S3 mode', async () => { diff --git a/apps/sim/app/api/files/upload/route.ts b/apps/sim/app/api/files/upload/route.ts index 405d5a2e96..8ec4e7e171 100644 --- a/apps/sim/app/api/files/upload/route.ts +++ b/apps/sim/app/api/files/upload/route.ts @@ -1,10 +1,6 @@ -import { writeFile } from 'fs/promises' -import { join } from 'path' import { type NextRequest, NextResponse } from 'next/server' -import { v4 as uuidv4 } from 'uuid' import { createLogger } from '@/lib/logs/console/logger' -import { isUsingCloudStorage, uploadFile } from '@/lib/uploads' -import { UPLOAD_DIR } from '@/lib/uploads/setup' +import { getPresignedUrl, isUsingCloudStorage, uploadFile } from '@/lib/uploads' import '@/lib/uploads/setup.server' import { createErrorResponse, @@ -27,10 +23,21 @@ export async function POST(request: NextRequest) { throw new InvalidRequestError('No files provided') } + // Get optional scoping parameters for execution-scoped storage + const workflowId = formData.get('workflowId') as string | null + const executionId = formData.get('executionId') as string | null + const workspaceId = formData.get('workspaceId') as string | null + // Log storage mode const usingCloudStorage = isUsingCloudStorage() logger.info(`Using storage mode: ${usingCloudStorage ? 'Cloud' : 'Local'} for file upload`) + if (workflowId && executionId) { + logger.info( + `Uploading files for execution-scoped storage: workflow=${workflowId}, execution=${executionId}` + ) + } + const uploadResults = [] // Process each file @@ -39,33 +46,60 @@ export async function POST(request: NextRequest) { const bytes = await file.arrayBuffer() const buffer = Buffer.from(bytes) - if (usingCloudStorage) { - // Upload to cloud storage (S3 or Azure Blob) - try { - logger.info(`Uploading file to cloud storage: ${originalName}`) - const result = await uploadFile(buffer, originalName, file.type, file.size) - logger.info(`Successfully uploaded to cloud storage: ${result.key}`) - uploadResults.push(result) - } catch (error) { - logger.error('Error uploading to cloud storage:', error) - throw error + // For execution-scoped files, use the dedicated execution file storage + if (workflowId && executionId) { + // Use the dedicated execution file storage system + const { uploadExecutionFile } = await import('@/lib/workflows/execution-file-storage') + const userFile = await uploadExecutionFile( + { + workspaceId: workspaceId || '', + workflowId, + executionId, + }, + buffer, + originalName, + file.type + ) + + uploadResults.push(userFile) + continue + } + + // Upload to cloud or local storage using the standard uploadFile function + try { + logger.info(`Uploading file: ${originalName}`) + const result = await uploadFile(buffer, originalName, file.type, file.size) + + // Generate a presigned URL for cloud storage with appropriate expiry + // Regular files get 24 hours (execution files are handled above) + let presignedUrl: string | undefined + if (usingCloudStorage) { + try { + presignedUrl = await getPresignedUrl(result.key, 24 * 60 * 60) // 24 hours + } catch (error) { + logger.warn(`Failed to generate presigned URL for ${originalName}:`, error) + } } - } else { - // Upload to local file system in development - const extension = originalName.split('.').pop() || '' - const uniqueFilename = `${uuidv4()}.${extension}` - const filePath = join(UPLOAD_DIR, uniqueFilename) - - logger.info(`Uploading file to local storage: ${filePath}`) - await writeFile(filePath, buffer) - logger.info(`Successfully wrote file to: ${filePath}`) - - uploadResults.push({ - path: `/api/files/serve/${uniqueFilename}`, + + // Create the serve path + const servePath = `/api/files/serve/${result.key}` + + const uploadResult = { name: originalName, size: file.size, type: file.type, - }) + key: result.key, + path: servePath, + url: presignedUrl || servePath, + uploadedAt: new Date().toISOString(), + expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), // 24 hours + } + + logger.info(`Successfully uploaded: ${result.key}`) + uploadResults.push(uploadResult) + } catch (error) { + logger.error(`Error uploading ${originalName}:`, error) + throw error } } diff --git a/apps/sim/app/api/help/route.ts b/apps/sim/app/api/help/route.ts index e1bb6f8234..e21ab0353c 100644 --- a/apps/sim/app/api/help/route.ts +++ b/apps/sim/app/api/help/route.ts @@ -98,8 +98,8 @@ ${message} // Send email using Resend const { data, error } = await resend.emails.send({ - from: `Sim `, - to: [`help@${getEmailDomain()}`], + from: `Sim `, + to: [`help@${env.EMAIL_DOMAIN || getEmailDomain()}`], subject: `[${type.toUpperCase()}] ${subject}`, replyTo: email, text: emailText, @@ -121,7 +121,7 @@ ${message} // Send confirmation email to the user await resend.emails .send({ - from: `Sim `, + from: `Sim `, to: [email], subject: `Your ${type} request has been received: ${subject}`, text: ` @@ -137,7 +137,7 @@ ${images.length > 0 ? `You attached ${images.length} image(s).` : ''} Best regards, The Sim Team `, - replyTo: `help@${getEmailDomain()}`, + replyTo: `help@${env.EMAIL_DOMAIN || getEmailDomain()}`, }) .catch((err) => { logger.warn(`[${requestId}] Failed to send confirmation email`, err) diff --git a/apps/sim/app/api/knowledge/[id]/documents/route.test.ts b/apps/sim/app/api/knowledge/[id]/documents/route.test.ts index c27bec231d..61a702cc72 100644 --- a/apps/sim/app/api/knowledge/[id]/documents/route.test.ts +++ b/apps/sim/app/api/knowledge/[id]/documents/route.test.ts @@ -376,7 +376,7 @@ describe('Knowledge Base Documents API Route', () => { ], processingOptions: { chunkSize: 50, // Invalid: too small - minCharactersPerChunk: 10, // Invalid: too small + minCharactersPerChunk: 0, // Invalid: too small recipe: 'default', lang: 'en', chunkOverlap: 1000, // Invalid: too large diff --git a/apps/sim/app/api/knowledge/[id]/documents/route.ts b/apps/sim/app/api/knowledge/[id]/documents/route.ts index c3b14ac4a7..4c9813a02e 100644 --- a/apps/sim/app/api/knowledge/[id]/documents/route.ts +++ b/apps/sim/app/api/knowledge/[id]/documents/route.ts @@ -295,7 +295,7 @@ const BulkCreateDocumentsSchema = z.object({ documents: z.array(CreateDocumentSchema), processingOptions: z.object({ chunkSize: z.number().min(100).max(4000), - minCharactersPerChunk: z.number().min(50).max(2000), + minCharactersPerChunk: z.number().min(1).max(2000), recipe: z.string(), lang: z.string(), chunkOverlap: z.number().min(0).max(500), diff --git a/apps/sim/app/api/knowledge/route.test.ts b/apps/sim/app/api/knowledge/route.test.ts index 97218d77c0..0d3d81fcd1 100644 --- a/apps/sim/app/api/knowledge/route.test.ts +++ b/apps/sim/app/api/knowledge/route.test.ts @@ -168,7 +168,7 @@ describe('Knowledge Base API Route', () => { expect(data.data.embeddingDimension).toBe(1536) expect(data.data.chunkingConfig).toEqual({ maxSize: 1024, - minSize: 100, + minSize: 1, overlap: 200, }) }) diff --git a/apps/sim/app/api/knowledge/route.ts b/apps/sim/app/api/knowledge/route.ts index 0f73ea5bf0..a4f5b2dd08 100644 --- a/apps/sim/app/api/knowledge/route.ts +++ b/apps/sim/app/api/knowledge/route.ts @@ -18,12 +18,12 @@ const CreateKnowledgeBaseSchema = z.object({ chunkingConfig: z .object({ maxSize: z.number().min(100).max(4000).default(1024), - minSize: z.number().min(50).max(2000).default(100), + minSize: z.number().min(1).max(2000).default(1), overlap: z.number().min(0).max(500).default(200), }) .default({ maxSize: 1024, - minSize: 100, + minSize: 1, overlap: 200, }) .refine((data) => data.minSize < data.maxSize, { diff --git a/apps/sim/app/api/knowledge/utils.ts b/apps/sim/app/api/knowledge/utils.ts index 4e9b1a158b..448f6f53d1 100644 --- a/apps/sim/app/api/knowledge/utils.ts +++ b/apps/sim/app/api/knowledge/utils.ts @@ -531,7 +531,8 @@ export async function processDocumentAsync( docData.filename, docData.mimeType, processingOptions.chunkSize || 1000, - processingOptions.chunkOverlap || 200 + processingOptions.chunkOverlap || 200, + processingOptions.minCharactersPerChunk || 1 ) const now = new Date() diff --git a/apps/sim/app/api/logs/cleanup/route.ts b/apps/sim/app/api/logs/cleanup/route.ts index 5e7b9257af..823a8d7c66 100644 --- a/apps/sim/app/api/logs/cleanup/route.ts +++ b/apps/sim/app/api/logs/cleanup/route.ts @@ -5,7 +5,8 @@ import { verifyCronAuth } from '@/lib/auth/internal' import { env } from '@/lib/env' import { createLogger } from '@/lib/logs/console/logger' import { snapshotService } from '@/lib/logs/execution/snapshot/service' -import { getS3Client } from '@/lib/uploads/s3/s3-client' +import { deleteFile, isUsingCloudStorage } from '@/lib/uploads' +// Dynamic import for S3 client to avoid client-side bundling import { db } from '@/db' import { subscription, user, workflow, workflowExecutionLogs } from '@/db/schema' @@ -69,6 +70,11 @@ export async function GET(request: NextRequest) { deleted: 0, deleteFailed: 0, }, + files: { + total: 0, + deleted: 0, + deleteFailed: 0, + }, snapshots: { cleaned: 0, cleanupFailed: 0, @@ -106,6 +112,7 @@ export async function GET(request: NextRequest) { totalInputCost: workflowExecutionLogs.totalInputCost, totalOutputCost: workflowExecutionLogs.totalOutputCost, totalTokens: workflowExecutionLogs.totalTokens, + files: workflowExecutionLogs.files, metadata: workflowExecutionLogs.metadata, createdAt: workflowExecutionLogs.createdAt, }) @@ -132,6 +139,7 @@ export async function GET(request: NextRequest) { }) try { + const { getS3Client } = await import('@/lib/uploads/s3/s3-client') await getS3Client().send( new PutObjectCommand({ Bucket: S3_CONFIG.bucket, @@ -150,6 +158,23 @@ export async function GET(request: NextRequest) { results.enhancedLogs.archived++ + // Clean up associated files if using cloud storage + if (isUsingCloudStorage() && log.files && Array.isArray(log.files)) { + for (const file of log.files) { + if (file && typeof file === 'object' && file.key) { + results.files.total++ + try { + await deleteFile(file.key) + results.files.deleted++ + logger.info(`Deleted file: ${file.key}`) + } catch (fileError) { + results.files.deleteFailed++ + logger.error(`Failed to delete file ${file.key}:`, { fileError }) + } + } + } + } + try { // Delete enhanced log const deleteResult = await db @@ -198,7 +223,7 @@ export async function GET(request: NextRequest) { const reachedLimit = batchesProcessed >= MAX_BATCHES && hasMoreLogs return NextResponse.json({ - message: `Processed ${batchesProcessed} enhanced log batches (${results.enhancedLogs.total} logs) in ${timeElapsed.toFixed(2)}s${reachedLimit ? ' (batch limit reached)' : ''}`, + message: `Processed ${batchesProcessed} enhanced log batches (${results.enhancedLogs.total} logs, ${results.files.total} files) in ${timeElapsed.toFixed(2)}s${reachedLimit ? ' (batch limit reached)' : ''}`, results, complete: !hasMoreLogs, batchLimitReached: reachedLimit, diff --git a/apps/sim/app/api/logs/route.ts b/apps/sim/app/api/logs/route.ts index ff3fd92bf7..e559ca3e06 100644 --- a/apps/sim/app/api/logs/route.ts +++ b/apps/sim/app/api/logs/route.ts @@ -95,6 +95,7 @@ export async function GET(request: NextRequest) { totalOutputCost: workflowExecutionLogs.totalOutputCost, totalTokens: workflowExecutionLogs.totalTokens, metadata: workflowExecutionLogs.metadata, + files: workflowExecutionLogs.files, createdAt: workflowExecutionLogs.createdAt, workflowName: workflow.name, workflowDescription: workflow.description, @@ -334,6 +335,7 @@ export async function GET(request: NextRequest) { duration: log.totalDurationMs ? `${log.totalDurationMs}ms` : null, trigger: log.trigger, createdAt: log.startedAt.toISOString(), + files: log.files || undefined, workflow: params.includeWorkflow ? workflow : undefined, metadata: { totalDuration: log.totalDurationMs, diff --git a/apps/sim/app/api/organizations/[id]/invitations/route.ts b/apps/sim/app/api/organizations/[id]/invitations/route.ts index 1acae5939b..6537061a74 100644 --- a/apps/sim/app/api/organizations/[id]/invitations/route.ts +++ b/apps/sim/app/api/organizations/[id]/invitations/route.ts @@ -12,7 +12,7 @@ import { validateSeatAvailability, } from '@/lib/billing/validation/seat-management' import { sendEmail } from '@/lib/email/mailer' -import { validateAndNormalizeEmail } from '@/lib/email/utils' +import { quickValidateEmail } from '@/lib/email/validation' import { env } from '@/lib/env' import { createLogger } from '@/lib/logs/console/logger' import { hasWorkspaceAdminAccess } from '@/lib/permissions/utils' @@ -201,8 +201,9 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{ // Validate and normalize emails const processedEmails = invitationEmails .map((email: string) => { - const result = validateAndNormalizeEmail(email) - return result.isValid ? result.normalized : null + const normalized = email.trim().toLowerCase() + const validation = quickValidateEmail(normalized) + return validation.isValid ? normalized : null }) .filter(Boolean) as string[] @@ -401,7 +402,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{ pendingEmails.includes(email) ), invalidEmails: invitationEmails.filter( - (email: string) => !validateAndNormalizeEmail(email) + (email: string) => !quickValidateEmail(email.trim().toLowerCase()).isValid ), workspaceInvitations: isBatch ? validWorkspaceInvitations.length : 0, seatInfo: { diff --git a/apps/sim/app/api/organizations/[id]/members/route.ts b/apps/sim/app/api/organizations/[id]/members/route.ts index 484d0878f0..9f40f187cf 100644 --- a/apps/sim/app/api/organizations/[id]/members/route.ts +++ b/apps/sim/app/api/organizations/[id]/members/route.ts @@ -5,7 +5,7 @@ import { getEmailSubject, renderInvitationEmail } from '@/components/emails/rend import { getSession } from '@/lib/auth' import { validateSeatAvailability } from '@/lib/billing/validation/seat-management' import { sendEmail } from '@/lib/email/mailer' -import { validateAndNormalizeEmail } from '@/lib/email/utils' +import { quickValidateEmail } from '@/lib/email/validation' import { env } from '@/lib/env' import { createLogger } from '@/lib/logs/console/logger' import { db } from '@/db' @@ -139,9 +139,13 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{ } // Validate and normalize email - const { isValid, normalized: normalizedEmail } = validateAndNormalizeEmail(email) - if (!isValid) { - return NextResponse.json({ error: 'Invalid email format' }, { status: 400 }) + const normalizedEmail = email.trim().toLowerCase() + const validation = quickValidateEmail(normalizedEmail) + if (!validation.isValid) { + return NextResponse.json( + { error: validation.reason || 'Invalid email format' }, + { status: 400 } + ) } // Verify user has admin access diff --git a/apps/sim/app/api/proxy/route.ts b/apps/sim/app/api/proxy/route.ts index 95c6d99975..46d69b20c0 100644 --- a/apps/sim/app/api/proxy/route.ts +++ b/apps/sim/app/api/proxy/route.ts @@ -177,7 +177,7 @@ export async function POST(request: Request) { throw new Error('Invalid JSON in request body') } - const { toolId, params } = requestBody + const { toolId, params, executionContext } = requestBody if (!toolId) { logger.error(`[${requestId}] Missing toolId in request`) @@ -214,8 +214,21 @@ export async function POST(request: Request) { }) } + // Check if tool has file outputs - if so, don't skip post-processing + const hasFileOutputs = + tool.outputs && + Object.values(tool.outputs).some( + (output) => output.type === 'file' || output.type === 'file[]' + ) + // Execute tool - const result = await executeTool(toolId, params, true, true) + const result = await executeTool( + toolId, + params, + true, // skipProxy (we're already in the proxy) + !hasFileOutputs, // skipPostProcess (don't skip if tool has file outputs) + executionContext // pass execution context for file processing + ) if (!result.success) { logger.warn(`[${requestId}] Tool execution failed for ${toolId}`, { diff --git a/apps/sim/app/api/schedules/[id]/route.ts b/apps/sim/app/api/schedules/[id]/route.ts index 28696d54b3..1296751005 100644 --- a/apps/sim/app/api/schedules/[id]/route.ts +++ b/apps/sim/app/api/schedules/[id]/route.ts @@ -3,6 +3,7 @@ import { eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { getSession } from '@/lib/auth' import { createLogger } from '@/lib/logs/console/logger' +import { getUserEntityPermissions } from '@/lib/permissions/utils' import { db } from '@/db' import { workflow, workflowSchedule } from '@/db/schema' @@ -36,6 +37,7 @@ export async function DELETE( workflow: { id: workflow.id, userId: workflow.userId, + workspaceId: workflow.workspaceId, }, }) .from(workflowSchedule) @@ -48,7 +50,22 @@ export async function DELETE( return NextResponse.json({ error: 'Schedule not found' }, { status: 404 }) } - if (schedules[0].workflow.userId !== session.user.id) { + const workflowRecord = schedules[0].workflow + + // Check authorization - either the user owns the workflow or has write/admin workspace permissions + let isAuthorized = workflowRecord.userId === session.user.id + + // If not authorized by ownership and the workflow belongs to a workspace, check workspace permissions + if (!isAuthorized && workflowRecord.workspaceId) { + const userPermission = await getUserEntityPermissions( + session.user.id, + 'workspace', + workflowRecord.workspaceId + ) + isAuthorized = userPermission === 'write' || userPermission === 'admin' + } + + if (!isAuthorized) { logger.warn(`[${requestId}] Unauthorized schedule deletion attempt for schedule: ${id}`) return NextResponse.json({ error: 'Unauthorized' }, { status: 403 }) } diff --git a/apps/sim/app/api/schedules/[id]/status/route.ts b/apps/sim/app/api/schedules/[id]/status/route.ts index e2df5b61a6..f79c4360b5 100644 --- a/apps/sim/app/api/schedules/[id]/status/route.ts +++ b/apps/sim/app/api/schedules/[id]/status/route.ts @@ -2,6 +2,7 @@ import { eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { getSession } from '@/lib/auth' import { createLogger } from '@/lib/logs/console/logger' +import { getUserEntityPermissions } from '@/lib/permissions/utils' export const dynamic = 'force-dynamic' @@ -42,7 +43,7 @@ export async function GET(req: NextRequest, { params }: { params: Promise<{ id: } const [workflowRecord] = await db - .select({ userId: workflow.userId }) + .select({ userId: workflow.userId, workspaceId: workflow.workspaceId }) .from(workflow) .where(eq(workflow.id, schedule.workflowId)) .limit(1) @@ -52,7 +53,20 @@ export async function GET(req: NextRequest, { params }: { params: Promise<{ id: return NextResponse.json({ error: 'Workflow not found' }, { status: 404 }) } - if (workflowRecord.userId !== session.user.id) { + // Check authorization - either the user owns the workflow or has workspace permissions + let isAuthorized = workflowRecord.userId === session.user.id + + // If not authorized by ownership and the workflow belongs to a workspace, check workspace permissions + if (!isAuthorized && workflowRecord.workspaceId) { + const userPermission = await getUserEntityPermissions( + session.user.id, + 'workspace', + workflowRecord.workspaceId + ) + isAuthorized = userPermission !== null + } + + if (!isAuthorized) { logger.warn(`[${requestId}] User not authorized to view this schedule: ${scheduleId}`) return NextResponse.json({ error: 'Not authorized to view this schedule' }, { status: 403 }) } diff --git a/apps/sim/app/api/schedules/execute/route.ts b/apps/sim/app/api/schedules/execute/route.ts index 0f6cd28389..38dc7802e1 100644 --- a/apps/sim/app/api/schedules/execute/route.ts +++ b/apps/sim/app/api/schedules/execute/route.ts @@ -209,244 +209,284 @@ export async function GET() { requestId ) - // Load workflow data from normalized tables (no fallback to deprecated state column) - logger.debug( - `[${requestId}] Loading workflow ${schedule.workflowId} from normalized tables` - ) - const normalizedData = await loadWorkflowFromNormalizedTables(schedule.workflowId) - - if (!normalizedData) { - logger.error( - `[${requestId}] No normalized data found for scheduled workflow ${schedule.workflowId}` - ) - throw new Error( - `Workflow data not found in normalized tables for ${schedule.workflowId}` + try { + // Load workflow data from normalized tables (no fallback to deprecated state column) + logger.debug( + `[${requestId}] Loading workflow ${schedule.workflowId} from normalized tables` ) - } + const normalizedData = await loadWorkflowFromNormalizedTables(schedule.workflowId) - // Use normalized data only - const blocks = normalizedData.blocks - const edges = normalizedData.edges - const loops = normalizedData.loops - const parallels = normalizedData.parallels - logger.info( - `[${requestId}] Loaded scheduled workflow ${schedule.workflowId} from normalized tables` - ) + if (!normalizedData) { + logger.error( + `[${requestId}] No normalized data found for scheduled workflow ${schedule.workflowId}` + ) + throw new Error( + `Workflow data not found in normalized tables for ${schedule.workflowId}` + ) + } - const mergedStates = mergeSubblockState(blocks) + // Use normalized data only + const blocks = normalizedData.blocks + const edges = normalizedData.edges + const loops = normalizedData.loops + const parallels = normalizedData.parallels + logger.info( + `[${requestId}] Loaded scheduled workflow ${schedule.workflowId} from normalized tables` + ) - // Retrieve environment variables for this user (if any). - const [userEnv] = await db - .select() - .from(environmentTable) - .where(eq(environmentTable.userId, workflowRecord.userId)) - .limit(1) + const mergedStates = mergeSubblockState(blocks) - if (!userEnv) { - logger.debug( - `[${requestId}] No environment record found for user ${workflowRecord.userId}. Proceeding with empty variables.` - ) - } + // Retrieve environment variables for this user (if any). + const [userEnv] = await db + .select() + .from(environmentTable) + .where(eq(environmentTable.userId, workflowRecord.userId)) + .limit(1) - const variables = EnvVarsSchema.parse(userEnv?.variables ?? {}) - - const currentBlockStates = await Object.entries(mergedStates).reduce( - async (accPromise, [id, block]) => { - const acc = await accPromise - acc[id] = await Object.entries(block.subBlocks).reduce( - async (subAccPromise, [key, subBlock]) => { - const subAcc = await subAccPromise - let value = subBlock.value - - if (typeof value === 'string' && value.includes('{{') && value.includes('}}')) { - const matches = value.match(/{{([^}]+)}}/g) - if (matches) { - for (const match of matches) { - const varName = match.slice(2, -2) - const encryptedValue = variables[varName] - if (!encryptedValue) { - throw new Error(`Environment variable "${varName}" was not found`) - } + if (!userEnv) { + logger.debug( + `[${requestId}] No environment record found for user ${workflowRecord.userId}. Proceeding with empty variables.` + ) + } - try { - const { decrypted } = await decryptSecret(encryptedValue) - value = (value as string).replace(match, decrypted) - } catch (error: any) { - logger.error( - `[${requestId}] Error decrypting value for variable "${varName}"`, - error - ) - throw new Error( - `Failed to decrypt environment variable "${varName}": ${error.message}` - ) + const variables = EnvVarsSchema.parse(userEnv?.variables ?? {}) + + const currentBlockStates = await Object.entries(mergedStates).reduce( + async (accPromise, [id, block]) => { + const acc = await accPromise + acc[id] = await Object.entries(block.subBlocks).reduce( + async (subAccPromise, [key, subBlock]) => { + const subAcc = await subAccPromise + let value = subBlock.value + + if ( + typeof value === 'string' && + value.includes('{{') && + value.includes('}}') + ) { + const matches = value.match(/{{([^}]+)}}/g) + if (matches) { + for (const match of matches) { + const varName = match.slice(2, -2) + const encryptedValue = variables[varName] + if (!encryptedValue) { + throw new Error(`Environment variable "${varName}" was not found`) + } + + try { + const { decrypted } = await decryptSecret(encryptedValue) + value = (value as string).replace(match, decrypted) + } catch (error: any) { + logger.error( + `[${requestId}] Error decrypting value for variable "${varName}"`, + error + ) + throw new Error( + `Failed to decrypt environment variable "${varName}": ${error.message}` + ) + } } } } - } - subAcc[key] = value - return subAcc - }, - Promise.resolve({} as Record) - ) - return acc - }, - Promise.resolve({} as Record>) - ) + subAcc[key] = value + return subAcc + }, + Promise.resolve({} as Record) + ) + return acc + }, + Promise.resolve({} as Record>) + ) - const decryptedEnvVars: Record = {} - for (const [key, encryptedValue] of Object.entries(variables)) { - try { - const { decrypted } = await decryptSecret(encryptedValue) - decryptedEnvVars[key] = decrypted - } catch (error: any) { - logger.error( - `[${requestId}] Failed to decrypt environment variable "${key}"`, - error - ) - throw new Error(`Failed to decrypt environment variable "${key}": ${error.message}`) + const decryptedEnvVars: Record = {} + for (const [key, encryptedValue] of Object.entries(variables)) { + try { + const { decrypted } = await decryptSecret(encryptedValue) + decryptedEnvVars[key] = decrypted + } catch (error: any) { + logger.error( + `[${requestId}] Failed to decrypt environment variable "${key}"`, + error + ) + throw new Error( + `Failed to decrypt environment variable "${key}": ${error.message}` + ) + } } - } - - // Process the block states to ensure response formats are properly parsed - const processedBlockStates = Object.entries(currentBlockStates).reduce( - (acc, [blockId, blockState]) => { - // Check if this block has a responseFormat that needs to be parsed - if (blockState.responseFormat && typeof blockState.responseFormat === 'string') { - const responseFormatValue = blockState.responseFormat.trim() - - // Check for variable references like - if (responseFormatValue.startsWith('<') && responseFormatValue.includes('>')) { - logger.debug( - `[${requestId}] Response format contains variable reference for block ${blockId}` - ) - // Keep variable references as-is - they will be resolved during execution - acc[blockId] = blockState - } else if (responseFormatValue === '') { - // Empty string - remove response format - acc[blockId] = { - ...blockState, - responseFormat: undefined, - } - } else { - try { - logger.debug(`[${requestId}] Parsing responseFormat for block ${blockId}`) - // Attempt to parse the responseFormat if it's a string - const parsedResponseFormat = JSON.parse(responseFormatValue) - acc[blockId] = { - ...blockState, - responseFormat: parsedResponseFormat, - } - } catch (error) { - logger.warn( - `[${requestId}] Failed to parse responseFormat for block ${blockId}, using undefined`, - error + // Process the block states to ensure response formats are properly parsed + const processedBlockStates = Object.entries(currentBlockStates).reduce( + (acc, [blockId, blockState]) => { + // Check if this block has a responseFormat that needs to be parsed + if (blockState.responseFormat && typeof blockState.responseFormat === 'string') { + const responseFormatValue = blockState.responseFormat.trim() + + // Check for variable references like + if (responseFormatValue.startsWith('<') && responseFormatValue.includes('>')) { + logger.debug( + `[${requestId}] Response format contains variable reference for block ${blockId}` ) - // Set to undefined instead of keeping malformed JSON - this allows execution to continue + // Keep variable references as-is - they will be resolved during execution + acc[blockId] = blockState + } else if (responseFormatValue === '') { + // Empty string - remove response format acc[blockId] = { ...blockState, responseFormat: undefined, } + } else { + try { + logger.debug(`[${requestId}] Parsing responseFormat for block ${blockId}`) + // Attempt to parse the responseFormat if it's a string + const parsedResponseFormat = JSON.parse(responseFormatValue) + + acc[blockId] = { + ...blockState, + responseFormat: parsedResponseFormat, + } + } catch (error) { + logger.warn( + `[${requestId}] Failed to parse responseFormat for block ${blockId}, using undefined`, + error + ) + // Set to undefined instead of keeping malformed JSON - this allows execution to continue + acc[blockId] = { + ...blockState, + responseFormat: undefined, + } + } } + } else { + acc[blockId] = blockState } - } else { - acc[blockId] = blockState - } - return acc - }, - {} as Record> - ) + return acc + }, + {} as Record> + ) - // Get workflow variables - let workflowVariables = {} - if (workflowRecord.variables) { - try { - if (typeof workflowRecord.variables === 'string') { - workflowVariables = JSON.parse(workflowRecord.variables) - } else { - workflowVariables = workflowRecord.variables + // Get workflow variables + let workflowVariables = {} + if (workflowRecord.variables) { + try { + if (typeof workflowRecord.variables === 'string') { + workflowVariables = JSON.parse(workflowRecord.variables) + } else { + workflowVariables = workflowRecord.variables + } + } catch (error) { + logger.error(`Failed to parse workflow variables: ${schedule.workflowId}`, error) } - } catch (error) { - logger.error(`Failed to parse workflow variables: ${schedule.workflowId}`, error) } - } - const serializedWorkflow = new Serializer().serializeWorkflow( - mergedStates, - edges, - loops, - parallels, - true // Enable validation during execution - ) + const serializedWorkflow = new Serializer().serializeWorkflow( + mergedStates, + edges, + loops, + parallels, + true // Enable validation during execution + ) - const input = { - workflowId: schedule.workflowId, - _context: { + const input = { workflowId: schedule.workflowId, - }, - } + _context: { + workflowId: schedule.workflowId, + }, + } - // Start logging with environment variables - await loggingSession.safeStart({ - userId: workflowRecord.userId, - workspaceId: workflowRecord.workspaceId || '', - variables: variables || {}, - }) - - const executor = new Executor( - serializedWorkflow, - processedBlockStates, - decryptedEnvVars, - input, - workflowVariables - ) + // Start logging with environment variables + await loggingSession.safeStart({ + userId: workflowRecord.userId, + workspaceId: workflowRecord.workspaceId || '', + variables: variables || {}, + }) - // Set up logging on the executor - loggingSession.setupExecutor(executor) + const executor = new Executor({ + workflow: serializedWorkflow, + currentBlockStates: processedBlockStates, + envVarValues: decryptedEnvVars, + workflowInput: input, + workflowVariables, + contextExtensions: { + executionId, + workspaceId: workflowRecord.workspaceId || '', + }, + }) - const result = await executor.execute( - schedule.workflowId, - schedule.blockId || undefined - ) + // Set up logging on the executor + loggingSession.setupExecutor(executor) - const executionResult = - 'stream' in result && 'execution' in result ? result.execution : result + const result = await executor.execute( + schedule.workflowId, + schedule.blockId || undefined + ) - logger.info(`[${requestId}] Workflow execution completed: ${schedule.workflowId}`, { - success: executionResult.success, - executionTime: executionResult.metadata?.duration, - }) + const executionResult = + 'stream' in result && 'execution' in result ? result.execution : result - if (executionResult.success) { - await updateWorkflowRunCounts(schedule.workflowId) + logger.info(`[${requestId}] Workflow execution completed: ${schedule.workflowId}`, { + success: executionResult.success, + executionTime: executionResult.metadata?.duration, + }) - try { - await db - .update(userStats) - .set({ - totalScheduledExecutions: sql`total_scheduled_executions + 1`, - lastActive: now, - }) - .where(eq(userStats.userId, workflowRecord.userId)) - - logger.debug(`[${requestId}] Updated user stats for scheduled execution`) - } catch (statsError) { - logger.error(`[${requestId}] Error updating user stats:`, statsError) + if (executionResult.success) { + await updateWorkflowRunCounts(schedule.workflowId) + + try { + await db + .update(userStats) + .set({ + totalScheduledExecutions: sql`total_scheduled_executions + 1`, + lastActive: now, + }) + .where(eq(userStats.userId, workflowRecord.userId)) + + logger.debug(`[${requestId}] Updated user stats for scheduled execution`) + } catch (statsError) { + logger.error(`[${requestId}] Error updating user stats:`, statsError) + } } - } - const { traceSpans, totalDuration } = buildTraceSpans(executionResult) + const { traceSpans, totalDuration } = buildTraceSpans(executionResult) - // Complete logging - await loggingSession.safeComplete({ - endedAt: new Date().toISOString(), - totalDurationMs: totalDuration || 0, - finalOutput: executionResult.output || {}, - traceSpans: (traceSpans || []) as any, - }) + // Complete logging + await loggingSession.safeComplete({ + endedAt: new Date().toISOString(), + totalDurationMs: totalDuration || 0, + finalOutput: executionResult.output || {}, + traceSpans: (traceSpans || []) as any, + }) + + return { success: executionResult.success, blocks, executionResult } + } catch (earlyError: any) { + // Handle errors that occur before workflow execution (e.g., missing data, env vars, etc.) + logger.error( + `[${requestId}] Early failure in scheduled workflow ${schedule.workflowId}`, + earlyError + ) + + // Create a minimal log entry for early failures + try { + await loggingSession.safeStart({ + userId: workflowRecord.userId, + workspaceId: workflowRecord.workspaceId || '', + variables: {}, + }) + + await loggingSession.safeCompleteWithError({ + message: `Schedule execution failed before workflow started: ${earlyError.message}`, + stackTrace: earlyError.stack, + }) + } catch (loggingError) { + logger.error( + `[${requestId}] Failed to create log entry for early schedule failure`, + loggingError + ) + } - return { success: executionResult.success, blocks, executionResult } + // Re-throw the error to be handled by the outer catch block + throw earlyError + } })() if (executionSuccess.success) { @@ -535,7 +575,31 @@ export async function GET() { error ) - // Error logging handled by logging session inside sync executor + // Ensure we create a log entry for this failed execution + try { + const failureLoggingSession = new LoggingSession( + schedule.workflowId, + executionId, + 'schedule', + requestId + ) + + await failureLoggingSession.safeStart({ + userId: workflowRecord.userId, + workspaceId: workflowRecord.workspaceId || '', + variables: {}, + }) + + await failureLoggingSession.safeCompleteWithError({ + message: `Schedule execution failed: ${error.message}`, + stackTrace: error.stack, + }) + } catch (loggingError) { + logger.error( + `[${requestId}] Failed to create log entry for failed schedule execution`, + loggingError + ) + } let nextRunAt: Date try { diff --git a/apps/sim/app/api/schedules/route.test.ts b/apps/sim/app/api/schedules/route.test.ts index 264a5d0932..6638820a52 100644 --- a/apps/sim/app/api/schedules/route.test.ts +++ b/apps/sim/app/api/schedules/route.test.ts @@ -27,6 +27,11 @@ describe('Schedule Configuration API Route', () => { }), })) + // Mock permissions + vi.doMock('@/lib/permissions/utils', () => ({ + getUserEntityPermissions: vi.fn().mockResolvedValue('admin'), // User has admin permissions + })) + // Extend sampleWorkflowState for scheduling const _workflowStateWithSchedule = { ...sampleWorkflowState, @@ -46,33 +51,39 @@ describe('Schedule Configuration API Route', () => { } // Create mock database with test schedules + // Mock the database to return workflow data for authorization check vi.doMock('@/db', () => { + let callCount = 0 const mockDb = { select: vi.fn().mockImplementation(() => ({ - from: vi.fn().mockImplementation((table: string) => { - if (table === 'workflow_schedule') { - return { - where: vi.fn().mockImplementation(() => ({ - limit: vi.fn().mockImplementation(() => [ + from: vi.fn().mockImplementation(() => ({ + where: vi.fn().mockImplementation(() => ({ + limit: vi.fn().mockImplementation(() => { + callCount++ + // First call: workflow lookup for authorization + if (callCount === 1) { + return [ { - id: 'schedule-id', - workflowId: 'workflow-id', + id: 'workflow-id', userId: 'user-id', - nextRunAt: new Date(), - lastRanAt: null, - cronExpression: '0 9 * * *', - triggerType: 'schedule', + workspaceId: null, // User owns the workflow directly }, - ]), - })), - } - } - return { - where: vi.fn().mockImplementation(() => ({ - limit: vi.fn().mockImplementation(() => []), - })), - } - }), + ] + } + // Second call: existing schedule lookup - return existing schedule for update test + return [ + { + id: 'existing-schedule-id', + workflowId: 'workflow-id', + blockId: 'starter-id', + cronExpression: '0 9 * * *', + nextRunAt: new Date(), + status: 'active', + }, + ] + }), + })), + })), })), insert: vi.fn().mockImplementation(() => ({ values: vi.fn().mockImplementation(() => ({ @@ -176,94 +187,6 @@ describe('Schedule Configuration API Route', () => { // Instead, we just verify that the response has the expected properties }) - /** - * Test updating an existing schedule - */ - it('should update an existing schedule', async () => { - // Setup the specific DB mock for this test - vi.doMock('@/db', () => { - const mockDb = { - select: vi.fn().mockImplementation(() => ({ - from: vi.fn().mockImplementation(() => ({ - where: vi.fn().mockImplementation(() => ({ - limit: vi.fn().mockImplementation(() => [ - { - id: 'schedule-id', - workflowId: 'workflow-id', - nextRunAt: new Date(), - cronExpression: '0 9 * * *', - }, - ]), - })), - })), - })), - insert: vi.fn().mockImplementation(() => ({ - values: vi.fn().mockImplementation(() => ({ - onConflictDoUpdate: vi.fn().mockResolvedValue({}), - })), - })), - delete: vi.fn().mockImplementation(() => ({ - where: vi.fn().mockResolvedValue([]), - })), - } - - return { db: mockDb } - }) - - // Create a mock request with updated schedule - const req = createMockRequest('POST', { - workflowId: 'workflow-id', - state: { - blocks: { - 'starter-id': { - type: 'starter', - subBlocks: { - startWorkflow: { value: 'schedule' }, - scheduleType: { value: 'daily' }, - scheduleTime: { value: '10:30' }, // Updated time - dailyTime: { value: '10:30' }, - }, - }, - }, - edges: [], - loops: {}, - }, - }) - - // Override the schedule utils mock for this test - vi.doMock('@/lib/schedules/utils', () => ({ - getScheduleTimeValues: vi.fn().mockReturnValue({ - scheduleTime: '10:30', - dailyTime: [10, 30], - }), - getSubBlockValue: vi.fn().mockImplementation((block: any, id: string) => { - const subBlocks = { - startWorkflow: 'schedule', - scheduleType: 'daily', - scheduleTime: '10:30', - dailyTime: '10:30', - } - return subBlocks[id as keyof typeof subBlocks] || '' - }), - generateCronExpression: vi.fn().mockReturnValue('0 10 * * *'), - calculateNextRunTime: vi.fn().mockReturnValue(new Date()), - BlockState: {}, - })) - - // Import the route handler after mocks are set up - const { POST } = await import('@/app/api/schedules/route') - - // Call the handler - const response = await POST(req) - - // Verify response - expect(response).toBeDefined() - expect(response.status).toBe(200) - - const responseData = await response.json() - expect(responseData).toHaveProperty('message', 'Schedule updated') - }) - /** * Test removing a schedule */ diff --git a/apps/sim/app/api/schedules/route.ts b/apps/sim/app/api/schedules/route.ts index d3896b92cd..f0bdef3101 100644 --- a/apps/sim/app/api/schedules/route.ts +++ b/apps/sim/app/api/schedules/route.ts @@ -4,6 +4,7 @@ import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { getSession } from '@/lib/auth' import { createLogger } from '@/lib/logs/console/logger' +import { getUserEntityPermissions } from '@/lib/permissions/utils' import { type BlockState, calculateNextRunTime, @@ -13,7 +14,7 @@ import { validateCronExpression, } from '@/lib/schedules/utils' import { db } from '@/db' -import { workflowSchedule } from '@/db/schema' +import { workflow, workflowSchedule } from '@/db/schema' const logger = createLogger('ScheduledAPI') @@ -87,6 +88,34 @@ export async function GET(req: NextRequest) { return NextResponse.json({ error: 'Missing workflowId parameter' }, { status: 400 }) } + // Check if user has permission to view this workflow + const [workflowRecord] = await db + .select({ userId: workflow.userId, workspaceId: workflow.workspaceId }) + .from(workflow) + .where(eq(workflow.id, workflowId)) + .limit(1) + + if (!workflowRecord) { + return NextResponse.json({ error: 'Workflow not found' }, { status: 404 }) + } + + // Check authorization - either the user owns the workflow or has workspace permissions + let isAuthorized = workflowRecord.userId === session.user.id + + // If not authorized by ownership and the workflow belongs to a workspace, check workspace permissions + if (!isAuthorized && workflowRecord.workspaceId) { + const userPermission = await getUserEntityPermissions( + session.user.id, + 'workspace', + workflowRecord.workspaceId + ) + isAuthorized = userPermission !== null + } + + if (!isAuthorized) { + return NextResponse.json({ error: 'Not authorized to view this workflow' }, { status: 403 }) + } + const now = Date.now() const lastLog = recentRequests.get(workflowId) || 0 const shouldLog = now - lastLog > LOGGING_THROTTLE_MS @@ -152,6 +181,38 @@ export async function POST(req: NextRequest) { logger.info(`[${requestId}] Processing schedule update for workflow ${workflowId}`) + // Check if user has permission to modify this workflow + const [workflowRecord] = await db + .select({ userId: workflow.userId, workspaceId: workflow.workspaceId }) + .from(workflow) + .where(eq(workflow.id, workflowId)) + .limit(1) + + if (!workflowRecord) { + logger.warn(`[${requestId}] Workflow not found: ${workflowId}`) + return NextResponse.json({ error: 'Workflow not found' }, { status: 404 }) + } + + // Check authorization - either the user owns the workflow or has write/admin workspace permissions + let isAuthorized = workflowRecord.userId === session.user.id + + // If not authorized by ownership and the workflow belongs to a workspace, check workspace permissions + if (!isAuthorized && workflowRecord.workspaceId) { + const userPermission = await getUserEntityPermissions( + session.user.id, + 'workspace', + workflowRecord.workspaceId + ) + isAuthorized = userPermission === 'write' || userPermission === 'admin' + } + + if (!isAuthorized) { + logger.warn( + `[${requestId}] User not authorized to modify schedule for workflow: ${workflowId}` + ) + return NextResponse.json({ error: 'Not authorized to modify this workflow' }, { status: 403 }) + } + // Find the target block - prioritize the specific blockId if provided let targetBlock: BlockState | undefined if (blockId) { diff --git a/apps/sim/app/api/webhooks/route.ts b/apps/sim/app/api/webhooks/route.ts index 9f38cb2d9f..cfcb29cfb9 100644 --- a/apps/sim/app/api/webhooks/route.ts +++ b/apps/sim/app/api/webhooks/route.ts @@ -87,7 +87,7 @@ export async function POST(request: NextRequest) { const { workflowId, path, provider, providerConfig, blockId } = body // Validate input - if (!workflowId || !path) { + if (!workflowId) { logger.warn(`[${requestId}] Missing required fields for webhook creation`, { hasWorkflowId: !!workflowId, hasPath: !!path, @@ -95,6 +95,26 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: 'Missing required fields' }, { status: 400 }) } + // For credential-based providers (those that use polling instead of webhooks), + // generate a dummy path if none provided since they don't use actual webhook URLs + // but still need database entries for the polling services to find them + let finalPath = path + if (!path || path.trim() === '') { + // List of providers that use credential-based polling instead of webhooks + const credentialBasedProviders = ['gmail', 'outlook'] + + if (credentialBasedProviders.includes(provider)) { + finalPath = `${provider}-${crypto.randomUUID()}` + logger.info(`[${requestId}] Generated dummy path for ${provider} trigger: ${finalPath}`) + } else { + logger.warn(`[${requestId}] Missing path for webhook creation`, { + hasWorkflowId: !!workflowId, + hasPath: !!path, + }) + return NextResponse.json({ error: 'Missing required path' }, { status: 400 }) + } + } + // Check if the workflow exists and user has permission to modify it const workflowData = await db .select({ @@ -144,29 +164,32 @@ export async function POST(request: NextRequest) { const existingWebhooks = await db .select({ id: webhook.id, workflowId: webhook.workflowId }) .from(webhook) - .where(eq(webhook.path, path)) + .where(eq(webhook.path, finalPath)) .limit(1) let savedWebhook: any = null // Variable to hold the result of save/update // If a webhook with the same path exists but belongs to a different workflow, return an error if (existingWebhooks.length > 0 && existingWebhooks[0].workflowId !== workflowId) { - logger.warn(`[${requestId}] Webhook path conflict: ${path}`) + logger.warn(`[${requestId}] Webhook path conflict: ${finalPath}`) return NextResponse.json( { error: 'Webhook path already exists.', code: 'PATH_EXISTS' }, { status: 409 } ) } + // Use the original provider config - Gmail/Outlook configuration functions will inject userId automatically + const finalProviderConfig = providerConfig + // If a webhook with the same path and workflowId exists, update it if (existingWebhooks.length > 0 && existingWebhooks[0].workflowId === workflowId) { - logger.info(`[${requestId}] Updating existing webhook for path: ${path}`) + logger.info(`[${requestId}] Updating existing webhook for path: ${finalPath}`) const updatedResult = await db .update(webhook) .set({ blockId, provider, - providerConfig, + providerConfig: finalProviderConfig, isActive: true, updatedAt: new Date(), }) @@ -183,9 +206,9 @@ export async function POST(request: NextRequest) { id: webhookId, workflowId, blockId, - path, + path: finalPath, provider, - providerConfig, + providerConfig: finalProviderConfig, isActive: true, createdAt: new Date(), updatedAt: new Date(), diff --git a/apps/sim/app/api/workflows/[id]/execute/route.test.ts b/apps/sim/app/api/workflows/[id]/execute/route.test.ts index 7be75d9f8a..351320f5b7 100644 --- a/apps/sim/app/api/workflows/[id]/execute/route.test.ts +++ b/apps/sim/app/api/workflows/[id]/execute/route.test.ts @@ -327,11 +327,14 @@ describe('Workflow Execution API Route', () => { expect(executeMock).toHaveBeenCalledWith('workflow-id') expect(Executor).toHaveBeenCalledWith( - expect.anything(), // serializedWorkflow - expect.anything(), // processedBlockStates - expect.anything(), // decryptedEnvVars - requestBody, // processedInput (direct input, not wrapped) - expect.anything() // workflowVariables + expect.objectContaining({ + workflow: expect.any(Object), // serializedWorkflow + currentBlockStates: expect.any(Object), // processedBlockStates + envVarValues: expect.any(Object), // decryptedEnvVars + workflowInput: requestBody, // processedInput (direct input, not wrapped) + workflowVariables: expect.any(Object), + contextExtensions: expect.any(Object), // Allow any context extensions object + }) ) }) @@ -363,11 +366,14 @@ describe('Workflow Execution API Route', () => { const Executor = (await import('@/executor')).Executor expect(Executor).toHaveBeenCalledWith( - expect.anything(), // serializedWorkflow - expect.anything(), // processedBlockStates - expect.anything(), // decryptedEnvVars - structuredInput, // processedInput (direct input, not wrapped) - expect.anything() // workflowVariables + expect.objectContaining({ + workflow: expect.any(Object), // serializedWorkflow + currentBlockStates: expect.any(Object), // processedBlockStates + envVarValues: expect.any(Object), // decryptedEnvVars + workflowInput: structuredInput, // processedInput (direct input, not wrapped) + workflowVariables: expect.any(Object), + contextExtensions: expect.any(Object), // Allow any context extensions object + }) ) }) @@ -391,11 +397,14 @@ describe('Workflow Execution API Route', () => { const Executor = (await import('@/executor')).Executor expect(Executor).toHaveBeenCalledWith( - expect.anything(), // serializedWorkflow - expect.anything(), // processedBlockStates - expect.anything(), // decryptedEnvVars - expect.objectContaining({}), // processedInput with empty input - expect.anything() // workflowVariables + expect.objectContaining({ + workflow: expect.any(Object), // serializedWorkflow + currentBlockStates: expect.any(Object), // processedBlockStates + envVarValues: expect.any(Object), // decryptedEnvVars + workflowInput: expect.objectContaining({}), // processedInput with empty input + workflowVariables: expect.any(Object), + contextExtensions: expect.any(Object), // Allow any context extensions object + }) ) }) @@ -585,8 +594,13 @@ describe('Workflow Execution API Route', () => { expect(executorCalls.length).toBeGreaterThan(0) const lastCall = executorCalls[executorCalls.length - 1] - expect(lastCall.length).toBeGreaterThanOrEqual(5) + expect(lastCall.length).toBeGreaterThanOrEqual(1) - expect(lastCall[4]).toEqual(workflowVariables) + // Check that workflowVariables are passed in the options object + expect(lastCall[0]).toEqual( + expect.objectContaining({ + workflowVariables: workflowVariables, + }) + ) }) }) diff --git a/apps/sim/app/api/workflows/[id]/execute/route.ts b/apps/sim/app/api/workflows/[id]/execute/route.ts index 927ae1f067..1fee2a4c76 100644 --- a/apps/sim/app/api/workflows/[id]/execute/route.ts +++ b/apps/sim/app/api/workflows/[id]/execute/route.ts @@ -278,13 +278,17 @@ async function executeWorkflow(workflow: any, requestId: string, input?: any): P true // Enable validation during execution ) - const executor = new Executor( - serializedWorkflow, - processedBlockStates, - decryptedEnvVars, - processedInput, - workflowVariables - ) + const executor = new Executor({ + workflow: serializedWorkflow, + currentBlockStates: processedBlockStates, + envVarValues: decryptedEnvVars, + workflowInput: processedInput, + workflowVariables, + contextExtensions: { + executionId, + workspaceId: workflow.workspaceId, + }, + }) // Set up logging on the executor loggingSession.setupExecutor(executor) diff --git a/apps/sim/app/api/workflows/[id]/state/route.ts b/apps/sim/app/api/workflows/[id]/state/route.ts index 75ba2f9b82..a1a788b607 100644 --- a/apps/sim/app/api/workflows/[id]/state/route.ts +++ b/apps/sim/app/api/workflows/[id]/state/route.ts @@ -51,6 +51,7 @@ const BlockStateSchema = z.object({ isWide: z.boolean().optional(), height: z.number().optional(), advancedMode: z.boolean().optional(), + triggerMode: z.boolean().optional(), data: BlockDataSchema.optional(), }) diff --git a/apps/sim/app/api/workflows/route.ts b/apps/sim/app/api/workflows/route.ts index 99eb109e1c..c4e56f8edf 100644 --- a/apps/sim/app/api/workflows/route.ts +++ b/apps/sim/app/api/workflows/route.ts @@ -130,6 +130,7 @@ export async function POST(req: NextRequest) { horizontalHandles: true, isWide: false, advancedMode: false, + triggerMode: false, height: 95, }, }, @@ -178,6 +179,7 @@ export async function POST(req: NextRequest) { horizontalHandles: true, isWide: false, advancedMode: false, + triggerMode: false, height: '95', subBlocks: { startWorkflow: { diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/upload-modal/upload-modal.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/upload-modal/upload-modal.tsx index ccb474fd94..0ebb3273bc 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/upload-modal/upload-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/upload-modal/upload-modal.tsx @@ -142,7 +142,7 @@ export function UploadModal({ try { await uploadFiles(files, knowledgeBaseId, { chunkSize: chunkingConfig?.maxSize || 1024, - minCharactersPerChunk: chunkingConfig?.minSize || 100, + minCharactersPerChunk: chunkingConfig?.minSize || 1, chunkOverlap: chunkingConfig?.overlap || 200, recipe: 'default', }) diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/components/create-modal/create-modal.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/components/create-modal/create-modal.tsx index 5eece7b5af..273a3c5419 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/components/create-modal/create-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/components/create-modal/create-modal.tsx @@ -50,7 +50,7 @@ const FormSchema = z description: z.string().max(500, 'Description must be less than 500 characters').optional(), minChunkSize: z .number() - .min(50, 'Min chunk size must be at least 50') + .min(1, 'Min chunk size must be at least 1') .max(2000, 'Min chunk size must be less than 2000'), maxChunkSize: z .number() @@ -115,7 +115,7 @@ export function CreateModal({ open, onOpenChange, onKnowledgeBaseCreated }: Crea defaultValues: { name: '', description: '', - minChunkSize: 100, + minChunkSize: 1, maxChunkSize: 1024, overlapSize: 200, }, @@ -299,7 +299,7 @@ export function CreateModal({ open, onOpenChange, onKnowledgeBaseCreated }: Crea reset({ name: '', description: '', - minChunkSize: 100, + minChunkSize: 1, maxChunkSize: 1024, overlapSize: 200, }) @@ -423,7 +423,7 @@ export function CreateModal({ open, onOpenChange, onKnowledgeBaseCreated }: Crea { + if (isDownloading) return + + setIsDownloading(true) + + try { + logger.info(`Initiating download for file: ${file.name}`) + + // Generate a fresh download URL + const response = await fetch('/api/files/download', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + key: file.key, + name: file.name, + storageProvider: file.storageProvider, + bucketName: file.bucketName, + isExecutionFile, // Add flag to indicate execution file + }), + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => ({ error: response.statusText })) + throw new Error(errorData.error || `Failed to generate download URL: ${response.status}`) + } + + const { downloadUrl, fileName } = await response.json() + + // Open the download URL in a new tab + window.open(downloadUrl, '_blank') + + logger.info(`Download initiated for file: ${fileName}`) + } catch (error) { + logger.error(`Failed to download file ${file.name}:`, error) + } finally { + setIsDownloading(false) + } + } + + return ( + + ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/sidebar/sidebar.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/sidebar/sidebar.tsx index eaeb89c497..69c5800f6a 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/sidebar/sidebar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/sidebar/sidebar.tsx @@ -9,6 +9,7 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/comp import { BASE_EXECUTION_CHARGE } from '@/lib/billing/constants' import { redactApiKeys } from '@/lib/utils' import { FrozenCanvasModal } from '@/app/workspace/[workspaceId]/logs/components/frozen-canvas/frozen-canvas-modal' +import { FileDownload } from '@/app/workspace/[workspaceId]/logs/components/sidebar/components/file-download' import LogMarkdownRenderer from '@/app/workspace/[workspaceId]/logs/components/sidebar/components/markdown-renderer' import { ToolCallsDisplay } from '@/app/workspace/[workspaceId]/logs/components/tool-calls/tool-calls-display' import { TraceSpansDisplay } from '@/app/workspace/[workspaceId]/logs/components/trace-spans/trace-spans-display' @@ -489,6 +490,36 @@ export function Sidebar({ )} + {/* Files */} + {log.files && log.files.length > 0 && ( +
+

+ Files ({log.files.length}) +

+
+ {log.files.map((file, index) => ( +
+
+
+ {file.name} +
+
+ {file.size ? `${Math.round(file.size / 1024)}KB` : 'Unknown size'} + {file.type && ` • ${file.type.split('/')[0]}`} +
+
+
+ +
+
+ ))} +
+
+ )} + {/* Frozen Canvas Button - only show for workflow execution logs with execution ID */} {isWorkflowExecutionLog && log.executionId && (
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/chat/chat.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/chat/chat.tsx index 8d04e43d55..637ea14381 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/chat/chat.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/chat/chat.tsx @@ -19,9 +19,18 @@ import { useExecutionStore } from '@/stores/execution/store' import { useChatStore } from '@/stores/panel/chat/store' import { useConsoleStore } from '@/stores/panel/console/store' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' +import { ChatFileUpload } from './components/chat-file-upload' const logger = createLogger('ChatPanel') +interface ChatFile { + id: string + name: string + size: number + type: string + file: File +} + interface ChatProps { panelWidth: number chatMessage: string @@ -51,6 +60,11 @@ export function Chat({ panelWidth, chatMessage, setChatMessage }: ChatProps) { const [promptHistory, setPromptHistory] = useState([]) const [historyIndex, setHistoryIndex] = useState(-1) + // File upload state + const [chatFiles, setChatFiles] = useState([]) + const [isUploadingFiles, setIsUploadingFiles] = useState(false) + const [dragCounter, setDragCounter] = useState(0) + const isDragOver = dragCounter > 0 // Scroll state const [isNearBottom, setIsNearBottom] = useState(true) const [showScrollButton, setShowScrollButton] = useState(false) @@ -211,13 +225,22 @@ export function Chat({ panelWidth, chatMessage, setChatMessage }: ChatProps) { // Handle send message const handleSendMessage = useCallback(async () => { - if (!chatMessage.trim() || !activeWorkflowId || isExecuting) return + if ( + (!chatMessage.trim() && chatFiles.length === 0) || + !activeWorkflowId || + isExecuting || + isUploadingFiles + ) + return // Store the message being sent for reference const sentMessage = chatMessage.trim() // Add to prompt history if it's not already the most recent - if (promptHistory.length === 0 || promptHistory[promptHistory.length - 1] !== sentMessage) { + if ( + sentMessage && + (promptHistory.length === 0 || promptHistory[promptHistory.length - 1] !== sentMessage) + ) { setPromptHistory((prev) => [...prev, sentMessage]) } @@ -232,23 +255,46 @@ export function Chat({ panelWidth, chatMessage, setChatMessage }: ChatProps) { // Get the conversationId for this workflow before adding the message const conversationId = getConversationId(activeWorkflowId) + let result: any = null - // Add user message - addMessage({ - content: sentMessage, - workflowId: activeWorkflowId, - type: 'user', - }) + try { + // Add user message + addMessage({ + content: + sentMessage || (chatFiles.length > 0 ? `Uploaded ${chatFiles.length} file(s)` : ''), + workflowId: activeWorkflowId, + type: 'user', + }) - // Clear input and refocus immediately - setChatMessage('') - focusInput(10) + // Prepare workflow input + const workflowInput: any = { + input: sentMessage, + conversationId: conversationId, + } - // Execute the workflow to generate a response, passing the chat message and conversationId as input - const result = await handleRunWorkflow({ - input: sentMessage, - conversationId: conversationId, - }) + // Add files if any (pass the File objects directly) + if (chatFiles.length > 0) { + workflowInput.files = chatFiles.map((chatFile) => ({ + name: chatFile.name, + size: chatFile.size, + type: chatFile.type, + file: chatFile.file, // Pass the actual File object + })) + } + + // Clear input and files, refocus immediately + setChatMessage('') + setChatFiles([]) + focusInput(10) + + // Execute the workflow to generate a response + result = await handleRunWorkflow(workflowInput) + } catch (error) { + logger.error('Error in handleSendMessage:', error) + setIsUploadingFiles(false) + // You might want to show an error message to the user here + return + } // Check if we got a streaming response if (result && 'stream' in result && result.stream instanceof ReadableStream) { @@ -541,7 +587,57 @@ export function Chat({ panelWidth, chatMessage, setChatMessage }: ChatProps) {
{/* Input section - Fixed height */} -
+
{ + e.preventDefault() + e.stopPropagation() + if (!(!activeWorkflowId || isExecuting || isUploadingFiles)) { + setDragCounter((prev) => prev + 1) + } + }} + onDragOver={(e) => { + e.preventDefault() + e.stopPropagation() + if (!(!activeWorkflowId || isExecuting || isUploadingFiles)) { + e.dataTransfer.dropEffect = 'copy' + } + }} + onDragLeave={(e) => { + e.preventDefault() + e.stopPropagation() + setDragCounter((prev) => Math.max(0, prev - 1)) + }} + onDrop={(e) => { + e.preventDefault() + e.stopPropagation() + setDragCounter(0) + if (!(!activeWorkflowId || isExecuting || isUploadingFiles)) { + const droppedFiles = Array.from(e.dataTransfer.files) + if (droppedFiles.length > 0) { + const newFiles = droppedFiles.slice(0, 5 - chatFiles.length).map((file) => ({ + id: crypto.randomUUID(), + name: file.name, + size: file.size, + type: file.type, + file, + })) + setChatFiles([...chatFiles, ...newFiles]) + } + } + }} + > + {/* File upload section */} +
+ +
+
+ + handleFileSelect(e.target.files)} + className='hidden' + accept={acceptedTypes.join(',')} + disabled={disabled} + /> + + {files.length > 0 && ( + + {files.length}/{maxFiles} files + + )} +
+ + {/* File List */} + {files.length > 0 && ( +
+ {files.map((file) => ( +
+ {getFileIcon(file.type)} + + {file.name} + + {formatFileSize(file.size)} + +
+ ))} +
+ )} + + {/* Drag and Drop Area (when dragging) */} + {isDragOver && ( +
+
+

Drop files here to attach

+
+
+ )} +
+ ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/user-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/user-input.tsx index b4e1dd12a2..c5177564f9 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/user-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/user-input.tsx @@ -323,12 +323,18 @@ const UserInput = forwardRef( } const handleFileSelect = () => { + if (disabled || isLoading) { + return + } + fileInputRef.current?.click() } const handleFileChange = async (e: React.ChangeEvent) => { const files = e.target.files - if (!files || files.length === 0) return + if (!files || files.length === 0) { + return + } await processFiles(files) @@ -554,6 +560,7 @@ const UserInput = forwardRef( className='hidden' accept='.pdf,.doc,.docx,.txt,.md,.png,.jpg,.jpeg,.gif,.svg' multiple + disabled={disabled || isLoading} />
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-upload.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-upload.tsx index bf4ab3b5d2..f932d7d8bd 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-upload.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-upload.tsx @@ -196,7 +196,12 @@ export function FileUpload({ } // Use the file info returned from the presigned URL endpoint - uploadedFiles.push(presignedData.fileInfo) + uploadedFiles.push({ + name: presignedData.fileInfo.name, + path: presignedData.fileInfo.path, + size: presignedData.fileInfo.size, + type: presignedData.fileInfo.type, + }) } else { // Fallback to traditional upload through API route useDirectUpload = false @@ -224,7 +229,7 @@ export function FileUpload({ uploadedFiles.push({ name: file.name, - path: data.path, + path: data.url || data.path, // Use url or path from upload response size: file.size, type: file.type, }) @@ -276,12 +281,12 @@ export function FileUpload({ if (multiple) { // For multiple files: Append to existing files if any const existingFiles = Array.isArray(value) ? value : value ? [value] : [] - // Create a map to identify duplicates by path + // Create a map to identify duplicates by url const uniqueFiles = new Map() // Add existing files to the map existingFiles.forEach((file) => { - uniqueFiles.set(file.path, file) + uniqueFiles.set(file.url || file.path, file) // Use url, fallback to path for backward compatibility }) // Add new files to the map (will overwrite if same path) @@ -327,7 +332,7 @@ export function FileUpload({ } // Mark this file as being deleted - setDeletingFiles((prev) => ({ ...prev, [file.path]: true })) + setDeletingFiles((prev) => ({ ...prev, [file.path || '']: true })) try { // Call API to delete the file from server @@ -366,7 +371,7 @@ export function FileUpload({ // Remove file from the deleting state setDeletingFiles((prev) => { const updated = { ...prev } - delete updated[file.path] + delete updated[file.path || ''] return updated }) } @@ -382,12 +387,11 @@ export function FileUpload({ if (!value) return const filesToDelete = Array.isArray(value) ? value : [value] - const _fileCount = filesToDelete.length // Mark all files as deleting const deletingStatus: Record = {} filesToDelete.forEach((file) => { - deletingStatus[file.path] = true + deletingStatus[file.path || ''] = true }) setDeletingFiles(deletingStatus) @@ -448,11 +452,12 @@ export function FileUpload({ // Helper to render a single file item const renderFileItem = (file: UploadedFile) => { - const isDeleting = deletingFiles[file.path] + const fileKey = file.path || '' + const isDeleting = deletingFiles[fileKey] return (
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/index.ts index c2f8c5d6c3..22aa0ef4b3 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/index.ts @@ -23,4 +23,5 @@ export { Switch } from './switch' export { Table } from './table' export { TimeInput } from './time-input' export { ToolInput } from './tool-input/tool-input' +export { TriggerConfig } from './trigger-config/trigger-config' export { WebhookConfig } from './webhook/webhook' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/trigger-config/components/trigger-config-section.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/trigger-config/components/trigger-config-section.tsx new file mode 100644 index 0000000000..333fc1ab13 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/trigger-config/components/trigger-config-section.tsx @@ -0,0 +1,360 @@ +import { useState } from 'react' +import { Check, ChevronDown, Copy, Eye, EyeOff, Info } from 'lucide-react' +import { Badge } from '@/components/ui/badge' +import { Button } from '@/components/ui/button' +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from '@/components/ui/command' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select' +import { Switch } from '@/components/ui/switch' +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip' +import { cn } from '@/lib/utils' +import type { TriggerConfig } from '@/triggers/types' + +interface TriggerConfigSectionProps { + triggerDef: TriggerConfig + config: Record + onChange: (fieldId: string, value: any) => void + webhookUrl: string + dynamicOptions?: Record | string[]> +} + +export function TriggerConfigSection({ + triggerDef, + config, + onChange, + webhookUrl, + dynamicOptions = {}, +}: TriggerConfigSectionProps) { + const [showSecrets, setShowSecrets] = useState>({}) + const [copied, setCopied] = useState(null) + + const copyToClipboard = (text: string, type: string) => { + navigator.clipboard.writeText(text) + setCopied(type) + setTimeout(() => setCopied(null), 2000) + } + + const toggleSecretVisibility = (fieldId: string) => { + setShowSecrets((prev) => ({ + ...prev, + [fieldId]: !prev[fieldId], + })) + } + + const renderField = (fieldId: string, fieldDef: any) => { + const value = config[fieldId] ?? fieldDef.defaultValue ?? '' + const isSecret = fieldDef.isSecret + const showSecret = showSecrets[fieldId] + + switch (fieldDef.type) { + case 'boolean': + return ( +
+ onChange(fieldId, checked)} + /> + +
+ ) + + case 'select': + return ( +
+ + + {fieldDef.description && ( +

{fieldDef.description}

+ )} +
+ ) + + case 'multiselect': { + const selectedValues = Array.isArray(value) ? value : [] + const rawOptions = dynamicOptions[fieldId] || fieldDef.options || [] + + // Handle both string[] and {id, name}[] formats + const availableOptions = rawOptions.map((option: any) => { + if (typeof option === 'string') { + return { id: option, name: option } + } + return option + }) + + // Create a map for quick lookup of display names + const optionMap = new Map(availableOptions.map((opt: any) => [opt.id, opt.name])) + + return ( +
+ + + + + + + + + + + {availableOptions.length === 0 + ? 'No options available. Please select credentials first.' + : 'No options found.'} + + + {availableOptions.map((option: any) => ( + { + const newValues = selectedValues.includes(option.id) + ? selectedValues.filter((v: string) => v !== option.id) + : [...selectedValues, option.id] + onChange(fieldId, newValues) + }} + > + + {option.name} + + ))} + + + + + + {fieldDef.description && ( +

{fieldDef.description}

+ )} +
+ ) + } + + case 'number': + return ( +
+ + onChange(fieldId, Number(e.target.value))} + /> + {fieldDef.description && ( +

{fieldDef.description}

+ )} +
+ ) + + default: // string + return ( +
+
+ + {fieldDef.description && ( + + + + + +

{fieldDef.description}

+
+
+ )} +
+
+
+ onChange(fieldId, e.target.value)} + className={cn( + 'h-10 flex-1', + isSecret ? 'pr-10' : '', + 'focus-visible:ring-2 focus-visible:ring-primary/20' + )} + /> + {isSecret && ( + + )} +
+ {isSecret && ( + + )} +
+
+ ) + } + } + + return ( +
+ {webhookUrl && ( +
+
+ + + + + + + +

This is the URL that will receive webhook requests

+
+
+
+
+
+
+ (e.target as HTMLInputElement).select()} + /> +
+ +
+
+ )} + + {Object.entries(triggerDef.configFields).map(([fieldId, fieldDef]) => ( +
{renderField(fieldId, fieldDef)}
+ ))} +
+ ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/trigger-config/components/trigger-instructions.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/trigger-config/components/trigger-instructions.tsx new file mode 100644 index 0000000000..2037d698ee --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/trigger-config/components/trigger-instructions.tsx @@ -0,0 +1,49 @@ +import { Notice } from '@/components/ui' +import { cn } from '@/lib/utils' +import { JSONView } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/console/components' +import type { TriggerConfig } from '@/triggers/types' + +interface TriggerInstructionsProps { + instructions: string[] + webhookUrl: string + samplePayload: any + triggerDef: TriggerConfig +} + +export function TriggerInstructions({ + instructions, + webhookUrl, + samplePayload, + triggerDef, +}: TriggerInstructionsProps) { + return ( +
+
+

Setup Instructions

+
+
    + {instructions.map((instruction, index) => ( +
  1. + ))} +
+
+
+ + + ) : null + } + title={`${triggerDef.provider.charAt(0).toUpperCase() + triggerDef.provider.slice(1)} Event Payload Example`} + > + Your workflow will receive a payload similar to this when a subscribed event occurs. +
+ +
+
+
+ ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/trigger-config/components/trigger-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/trigger-config/components/trigger-modal.tsx new file mode 100644 index 0000000000..1228e1cda8 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/trigger-config/components/trigger-modal.tsx @@ -0,0 +1,355 @@ +import { useEffect, useMemo, useState } from 'react' +import { Trash2 } from 'lucide-react' +import { Badge } from '@/components/ui/badge' +import { Button } from '@/components/ui/button' +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog' +import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' +import { createLogger } from '@/lib/logs/console/logger' +import { cn } from '@/lib/utils' +import { useSubBlockStore } from '@/stores/workflows/subblock/store' +import type { TriggerConfig } from '@/triggers/types' +import { CredentialSelector } from '../../credential-selector/credential-selector' +import { TriggerConfigSection } from './trigger-config-section' +import { TriggerInstructions } from './trigger-instructions' + +const logger = createLogger('TriggerModal') + +interface TriggerModalProps { + isOpen: boolean + onClose: () => void + triggerPath: string + triggerDef: TriggerConfig + triggerConfig: Record + onSave?: (path: string, config: Record) => Promise + onDelete?: () => Promise + triggerId?: string + blockId: string +} + +export function TriggerModal({ + isOpen, + onClose, + triggerPath, + triggerDef, + triggerConfig: initialConfig, + onSave, + onDelete, + triggerId, + blockId, +}: TriggerModalProps) { + const [config, setConfig] = useState>(initialConfig) + const [isSaving, setIsSaving] = useState(false) + + // Track if config has changed from initial values + const hasConfigChanged = useMemo(() => { + return JSON.stringify(config) !== JSON.stringify(initialConfig) + }, [config, initialConfig]) + const [isDeleting, setIsDeleting] = useState(false) + const [webhookUrl, setWebhookUrl] = useState('') + const [generatedPath, setGeneratedPath] = useState('') + const [hasCredentials, setHasCredentials] = useState(false) + const [selectedCredentialId, setSelectedCredentialId] = useState(null) + const [dynamicOptions, setDynamicOptions] = useState< + Record> + >({}) + + // Initialize config with default values from trigger definition + useEffect(() => { + const defaultConfig: Record = {} + + // Apply default values from trigger definition + Object.entries(triggerDef.configFields).forEach(([fieldId, field]) => { + if (field.defaultValue !== undefined && !(fieldId in initialConfig)) { + defaultConfig[fieldId] = field.defaultValue + } + }) + + // Merge with initial config, prioritizing initial config values + const mergedConfig = { ...defaultConfig, ...initialConfig } + + // Only update if there are actually default values to apply + if (Object.keys(defaultConfig).length > 0) { + setConfig(mergedConfig) + } + }, [triggerDef.configFields, initialConfig]) + + // Monitor credential selection + useEffect(() => { + if (triggerDef.requiresCredentials && triggerDef.credentialProvider) { + // Check if credentials are selected by monitoring the sub-block store + const checkCredentials = () => { + const subBlockStore = useSubBlockStore.getState() + const credentialValue = subBlockStore.getValue(blockId, 'triggerCredentials') + const hasCredential = Boolean(credentialValue) + setHasCredentials(hasCredential) + + // If credential changed and it's a Gmail trigger, load labels + if (hasCredential && credentialValue !== selectedCredentialId) { + setSelectedCredentialId(credentialValue) + if (triggerDef.provider === 'gmail') { + loadGmailLabels(credentialValue) + } + } + } + + checkCredentials() + + // Set up a subscription to monitor changes + const unsubscribe = useSubBlockStore.subscribe(checkCredentials) + + return unsubscribe + } + // If credentials aren't required, set to true + setHasCredentials(true) + }, [ + blockId, + triggerDef.requiresCredentials, + triggerDef.credentialProvider, + selectedCredentialId, + triggerDef.provider, + ]) + + // Load Gmail labels for the selected credential + const loadGmailLabels = async (credentialId: string) => { + try { + const response = await fetch(`/api/tools/gmail/labels?credentialId=${credentialId}`) + if (response.ok) { + const data = await response.json() + if (data.labels && Array.isArray(data.labels)) { + const labelOptions = data.labels.map((label: any) => ({ + id: label.id, + name: label.name, + })) + setDynamicOptions((prev) => ({ + ...prev, + labelIds: labelOptions, + })) + } + } else { + logger.error('Failed to load Gmail labels:', response.statusText) + } + } catch (error) { + logger.error('Error loading Gmail labels:', error) + } + } + + // Generate webhook path and URL + useEffect(() => { + // For triggers that don't use webhooks (like Gmail polling), skip URL generation + if (triggerDef.requiresCredentials && !triggerDef.webhook) { + setWebhookUrl('') + setGeneratedPath('') + return + } + + let finalPath = triggerPath + + // If no path exists, generate one automatically + if (!finalPath) { + const timestamp = Date.now() + const randomId = Math.random().toString(36).substring(2, 8) + finalPath = `/${triggerDef.provider}/${timestamp}-${randomId}` + setGeneratedPath(finalPath) + } + + if (finalPath) { + const baseUrl = window.location.origin + setWebhookUrl(`${baseUrl}/api/webhooks/trigger${finalPath}`) + } + }, [triggerPath, triggerDef.provider, triggerDef.requiresCredentials, triggerDef.webhook]) + + const handleConfigChange = (fieldId: string, value: any) => { + setConfig((prev) => ({ + ...prev, + [fieldId]: value, + })) + } + + const handleSave = async () => { + if (!onSave) return + + setIsSaving(true) + try { + // Use the existing trigger path or the generated one + const path = triggerPath || generatedPath + + // For credential-based triggers that don't use webhooks (like Gmail), path is optional + const requiresPath = triggerDef.webhook !== undefined + + if (requiresPath && !path) { + logger.error('No webhook path available for saving trigger') + return + } + + const success = await onSave(path || '', config) + if (success) { + onClose() + } + } catch (error) { + logger.error('Error saving trigger:', error) + } finally { + setIsSaving(false) + } + } + + const handleDelete = async () => { + if (!onDelete) return + + setIsDeleting(true) + try { + const success = await onDelete() + if (success) { + onClose() + } + } catch (error) { + logger.error('Error deleting trigger:', error) + } finally { + setIsDeleting(false) + } + } + + const isConfigValid = () => { + // Check if credentials are required and available + if (triggerDef.requiresCredentials && !hasCredentials) { + return false + } + + // Check required fields + for (const [fieldId, fieldDef] of Object.entries(triggerDef.configFields)) { + if (fieldDef.required && !config[fieldId]) { + return false + } + } + return true + } + + return ( + + e.preventDefault()} + > + +
+
+ + {triggerDef.name} Configuration + + {triggerId && ( + + + +
+
+
+
+ Active Trigger + + + +

{triggerDef.name}

+
+ + )} +
+
+ + +
+
+ {triggerDef.requiresCredentials && triggerDef.credentialProvider && ( +
+

Credentials

+

+ This trigger requires {triggerDef.credentialProvider.replace('-', ' ')}{' '} + credentials to access your account. +

+ +
+ )} + + + + +
+
+ + +
+
+ {triggerId && ( + + )} +
+
+ + +
+
+
+ +
+ ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/trigger-config/trigger-config.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/trigger-config/trigger-config.tsx new file mode 100644 index 0000000000..93dd8db710 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/trigger-config/trigger-config.tsx @@ -0,0 +1,403 @@ +import { useEffect, useState } from 'react' +import { ExternalLink } from 'lucide-react' +import { useParams } from 'next/navigation' +import { Button } from '@/components/ui/button' +import { createLogger } from '@/lib/logs/console/logger' +import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/hooks/use-sub-block-value' +import { useSubBlockStore } from '@/stores/workflows/subblock/store' +import { getTrigger } from '@/triggers' +import { TriggerModal } from './components/trigger-modal' + +const logger = createLogger('TriggerConfig') + +interface TriggerConfigProps { + blockId: string + isConnecting: boolean + isPreview?: boolean + value?: { + triggerId?: string + triggerPath?: string + triggerConfig?: Record + } + disabled?: boolean + availableTriggers?: string[] +} + +export function TriggerConfig({ + blockId, + isConnecting, + isPreview = false, + value: propValue, + disabled = false, + availableTriggers = [], +}: TriggerConfigProps) { + const [isModalOpen, setIsModalOpen] = useState(false) + const [isSaving, setIsSaving] = useState(false) + const [isDeleting, setIsDeleting] = useState(false) + const [error, setError] = useState(null) + const [triggerId, setTriggerId] = useState(null) + const params = useParams() + const workflowId = params.workflowId as string + const [isLoading, setIsLoading] = useState(false) + + // Get trigger configuration from the block state + const [storeTriggerProvider, setTriggerProvider] = useSubBlockValue(blockId, 'triggerProvider') + const [storeTriggerPath, setTriggerPath] = useSubBlockValue(blockId, 'triggerPath') + const [storeTriggerConfig, setTriggerConfig] = useSubBlockValue(blockId, 'triggerConfig') + const [storeTriggerId, setStoredTriggerId] = useSubBlockValue(blockId, 'triggerId') + + // Use prop values when available (preview mode), otherwise use store values + const selectedTriggerId = propValue?.triggerId ?? storeTriggerId ?? (availableTriggers[0] || null) + const triggerPath = propValue?.triggerPath ?? storeTriggerPath + const triggerConfig = propValue?.triggerConfig ?? storeTriggerConfig + + // Consolidate trigger ID logic + const effectiveTriggerId = selectedTriggerId || availableTriggers[0] + const triggerDef = effectiveTriggerId ? getTrigger(effectiveTriggerId) : null + + // Set the trigger ID to the first available one if none is set + useEffect(() => { + if (!selectedTriggerId && availableTriggers[0] && !isPreview) { + setStoredTriggerId(availableTriggers[0]) + } + }, [availableTriggers, selectedTriggerId, setStoredTriggerId, isPreview]) + + // Store the actual trigger from the database + const [actualTriggerId, setActualTriggerId] = useState(null) + + // Check if webhook exists in the database (using existing webhook API) + useEffect(() => { + // Skip API calls in preview mode + if (isPreview) { + setIsLoading(false) + return + } + + const checkWebhook = async () => { + setIsLoading(true) + try { + // Check if there's a webhook for this specific block + const response = await fetch(`/api/webhooks?workflowId=${workflowId}&blockId=${blockId}`) + if (response.ok) { + const data = await response.json() + if (data.webhooks && data.webhooks.length > 0) { + const webhook = data.webhooks[0].webhook + setTriggerId(webhook.id) + setActualTriggerId(webhook.provider) + + // Update the path in the block state if it's different + if (webhook.path && webhook.path !== triggerPath) { + setTriggerPath(webhook.path) + } + + // Update trigger config (from webhook providerConfig) + if (webhook.providerConfig) { + setTriggerConfig(webhook.providerConfig) + } + } else { + setTriggerId(null) + setActualTriggerId(null) + + // Clear stale trigger data from store when no webhook found in database + if (triggerPath) { + setTriggerPath('') + logger.info('Cleared stale trigger path on page refresh - no webhook in database', { + blockId, + clearedPath: triggerPath, + }) + } + } + } + } catch (error) { + logger.error('Error checking webhook:', { error }) + } finally { + setIsLoading(false) + } + } + + if (effectiveTriggerId) { + checkWebhook() + } + }, [workflowId, blockId, isPreview, effectiveTriggerId]) + + const handleOpenModal = () => { + if (isPreview || disabled) return + setIsModalOpen(true) + setError(null) + } + + const handleCloseModal = () => { + setIsModalOpen(false) + } + + const handleSaveTrigger = async (path: string, config: Record) => { + if (isPreview || disabled || !effectiveTriggerId) return false + + try { + setIsSaving(true) + setError(null) + + // Get trigger definition to check if it requires webhooks + const triggerDef = getTrigger(effectiveTriggerId) + if (!triggerDef) { + throw new Error('Trigger definition not found') + } + + // Set the trigger path and config in the block state + if (path && path !== triggerPath) { + setTriggerPath(path) + } + setTriggerConfig(config) + setStoredTriggerId(effectiveTriggerId) + + // Map trigger ID to webhook provider name + const webhookProvider = effectiveTriggerId.replace(/_webhook|_poller$/, '') // e.g., 'slack_webhook' -> 'slack', 'gmail_poller' -> 'gmail' + + // For credential-based triggers (like Gmail), create webhook entry for polling service but no webhook URL + if (triggerDef.requiresCredentials && !triggerDef.webhook) { + // Gmail polling service requires a webhook database entry to find the configuration + const response = await fetch('/api/webhooks', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + workflowId, + blockId, + path: '', // Empty path - API will generate dummy path for Gmail + provider: webhookProvider, + providerConfig: config, + }), + }) + + if (!response.ok) { + const errorData = await response.json() + throw new Error( + typeof errorData.error === 'object' + ? errorData.error.message || JSON.stringify(errorData.error) + : errorData.error || 'Failed to save credential-based trigger' + ) + } + + const data = await response.json() + const savedWebhookId = data.webhook.id + setTriggerId(savedWebhookId) + + logger.info('Credential-based trigger saved successfully', { + webhookId: savedWebhookId, + triggerDefId: effectiveTriggerId, + provider: webhookProvider, + blockId, + }) + + // Update the actual trigger after saving + setActualTriggerId(webhookProvider) + return true + } + + // Save as webhook using existing webhook API (for webhook-based triggers) + const response = await fetch('/api/webhooks', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + workflowId, + blockId, + path, + provider: webhookProvider, + providerConfig: config, + }), + }) + + if (!response.ok) { + const errorData = await response.json() + throw new Error( + typeof errorData.error === 'object' + ? errorData.error.message || JSON.stringify(errorData.error) + : errorData.error || 'Failed to save trigger' + ) + } + + const data = await response.json() + const savedWebhookId = data.webhook.id + setTriggerId(savedWebhookId) + + logger.info('Trigger saved successfully as webhook', { + webhookId: savedWebhookId, + triggerDefId: effectiveTriggerId, + provider: webhookProvider, + path, + blockId, + }) + + // Update the actual trigger after saving + setActualTriggerId(webhookProvider) + + return true + } catch (error: any) { + logger.error('Error saving trigger:', { error }) + setError(error.message || 'Failed to save trigger configuration') + return false + } finally { + setIsSaving(false) + } + } + + const handleDeleteTrigger = async () => { + if (isPreview || disabled || !triggerId) return false + + try { + setIsDeleting(true) + setError(null) + + // Delete webhook using existing webhook API (works for both webhook and credential-based triggers) + const response = await fetch(`/api/webhooks/${triggerId}`, { + method: 'DELETE', + }) + + if (!response.ok) { + const errorData = await response.json() + throw new Error(errorData.error || 'Failed to delete trigger') + } + + // Remove trigger-specific fields from the block state + const store = useSubBlockStore.getState() + const workflowValues = store.workflowValues[workflowId] || {} + const blockValues = { ...workflowValues[blockId] } + + // Remove trigger-related fields + blockValues.triggerId = undefined + blockValues.triggerConfig = undefined + blockValues.triggerPath = undefined + + // Update the store with the cleaned block values + useSubBlockStore.setState({ + workflowValues: { + ...workflowValues, + [workflowId]: { + ...workflowValues, + [blockId]: blockValues, + }, + }, + }) + + // Clear component state + setTriggerId(null) + setActualTriggerId(null) + + // Also clear store values using the setters to ensure UI updates + setTriggerPath('') + setTriggerConfig({}) + setStoredTriggerId('') + + logger.info('Trigger deleted successfully', { + blockId, + triggerType: + triggerDef?.requiresCredentials && !triggerDef.webhook + ? 'credential-based' + : 'webhook-based', + hadWebhookId: Boolean(triggerId), + }) + + handleCloseModal() + + return true + } catch (error: any) { + logger.error('Error deleting trigger:', { error }) + setError(error.message || 'Failed to delete trigger') + return false + } finally { + setIsDeleting(false) + } + } + + // Check if the trigger is connected + // Both webhook and credential-based triggers now have webhook database entries + const isTriggerConnected = Boolean(triggerId && actualTriggerId) + + // Debug logging to help with troubleshooting + useEffect(() => { + logger.info('Trigger connection status:', { + triggerId, + actualTriggerId, + triggerPath, + isTriggerConnected, + effectiveTriggerId, + triggerConfig, + triggerConfigKeys: triggerConfig ? Object.keys(triggerConfig) : [], + isCredentialBased: triggerDef?.requiresCredentials && !triggerDef.webhook, + storeValues: { + storeTriggerId, + storeTriggerPath, + storeTriggerConfig, + }, + }) + }, [ + triggerId, + actualTriggerId, + triggerPath, + isTriggerConnected, + effectiveTriggerId, + triggerConfig, + triggerDef, + storeTriggerId, + storeTriggerPath, + storeTriggerConfig, + ]) + + return ( +
+ {error &&
{error}
} + + {isTriggerConnected ? ( +
+
+
+
+ {triggerDef?.icon && ( + + )} + {triggerDef?.name || 'Active Trigger'} +
+
+
+
+ ) : ( + + )} + + {isModalOpen && triggerDef && ( + + )} +
+ ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/sub-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/sub-block.tsx index ccc99433d9..50a4f0acc1 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/sub-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/sub-block.tsx @@ -29,6 +29,7 @@ import { Table, TimeInput, ToolInput, + TriggerConfig, WebhookConfig, } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components' import type { SubBlockConfig } from '@/blocks/types' @@ -304,6 +305,27 @@ export function SubBlock({ /> ) } + case 'trigger-config': { + // For trigger config, we need to construct the value from multiple subblock values + const triggerValue = + isPreview && subBlockValues + ? { + triggerId: subBlockValues.triggerId?.value, + triggerPath: subBlockValues.triggerPath?.value, + triggerConfig: subBlockValues.triggerConfig?.value, + } + : previewValue + return ( + + ) + } case 'schedule-config': return ( ) { const blockWebhookStatus = !!(hasWebhookProvider && hasWebhookPath) const blockAdvancedMode = useWorkflowStore((state) => state.blocks[id]?.advancedMode ?? false) + const blockTriggerMode = useWorkflowStore((state) => state.blocks[id]?.triggerMode ?? false) // Collaborative workflow actions const { collaborativeUpdateBlockName, collaborativeToggleBlockWide, collaborativeToggleBlockAdvancedMode, + collaborativeToggleBlockTriggerMode, } = useCollaborativeWorkflow() // Workflow store actions @@ -394,11 +396,24 @@ export function WorkflowBlock({ id, data }: NodeProps) { } const isAdvancedMode = useWorkflowStore.getState().blocks[blockId]?.advancedMode ?? false + const isTriggerMode = useWorkflowStore.getState().blocks[blockId]?.triggerMode ?? false // Filter visible blocks and those that meet their conditions const visibleSubBlocks = subBlocks.filter((block) => { if (block.hidden) return false + // Special handling for trigger mode + if (block.type === ('trigger-config' as SubBlockType)) { + // Show trigger-config blocks when in trigger mode OR for pure trigger blocks + const isPureTriggerBlock = config?.triggers?.enabled && config.category === 'triggers' + return isTriggerMode || isPureTriggerBlock + } + + if (isTriggerMode && block.type !== ('trigger-config' as SubBlockType)) { + // In trigger mode, hide all non-trigger-config blocks + return false + } + // Filter by mode if specified if (block.mode) { if (block.mode === 'basic' && isAdvancedMode) return false @@ -550,8 +565,8 @@ export function WorkflowBlock({ id, data }: NodeProps) { )} - {/* Connection Blocks - Don't show for trigger blocks or starter blocks */} - {config.category !== 'triggers' && type !== 'starter' && ( + {/* Connection Blocks - Don't show for trigger blocks, starter blocks, or blocks in trigger mode */} + {config.category !== 'triggers' && type !== 'starter' && !blockTriggerMode && ( ) { /> )} - {/* Input Handle - Don't show for trigger blocks or starter blocks */} - {config.category !== 'triggers' && type !== 'starter' && ( + {/* Input Handle - Don't show for trigger blocks, starter blocks, or blocks in trigger mode */} + {config.category !== 'triggers' && type !== 'starter' && !blockTriggerMode && ( ) { )} + {/* Trigger Mode Button - Show for hybrid blocks that support triggers (not pure trigger blocks) */} + {config.triggers?.enabled && config.category !== 'triggers' && ( + + + + + + {!userPermissions.canEdit + ? userPermissions.isOfflineMode + ? 'Connection lost - please refresh' + : 'Read-only mode' + : blockTriggerMode + ? 'Switch to Action Mode' + : 'Switch to Trigger Mode'} + + + )} {config.docsLink ? ( @@ -925,8 +974,8 @@ export function WorkflowBlock({ id, data }: NodeProps) { isValidConnection={(connection) => connection.target !== id} /> - {/* Error Handle - Don't show for trigger blocks or starter blocks */} - {config.category !== 'triggers' && type !== 'starter' && ( + {/* Error Handle - Don't show for trigger blocks, starter blocks, or blocks in trigger mode */} + {config.category !== 'triggers' && type !== 'starter' && !blockTriggerMode && ( onStream?: (streamingExecution: StreamingExecution) => Promise executionId?: string + workspaceId?: string } } @@ -44,7 +45,7 @@ interface DebugValidationResult { export function useWorkflowExecution() { const currentWorkflow = useCurrentWorkflow() - const { activeWorkflowId } = useWorkflowRegistry() + const { activeWorkflowId, workflows } = useWorkflowRegistry() const { toggleConsole } = useConsoleStore() const { getAllVariables } = useEnvironmentStore() const { isDebugModeEnabled } = useGeneralStore() @@ -246,6 +247,14 @@ export function useWorkflowExecution() { async (workflowInput?: any, enableDebug = false) => { if (!activeWorkflowId) return + // Get workspaceId from workflow metadata + const workspaceId = workflows[activeWorkflowId]?.workspaceId + + if (!workspaceId) { + logger.error('Cannot execute workflow without workspaceId') + return + } + // Reset execution result and set execution state setExecutionResult(null) setIsExecuting(true) @@ -268,6 +277,78 @@ export function useWorkflowExecution() { const streamedContent = new Map() const streamReadingPromises: Promise[] = [] + // Handle file uploads if present + const uploadedFiles: any[] = [] + console.log('Checking for files to upload:', workflowInput.files) + if (workflowInput.files && Array.isArray(workflowInput.files)) { + try { + console.log('Processing files for upload:', workflowInput.files.length) + + for (const fileData of workflowInput.files) { + console.log('Uploading file:', fileData.name, fileData.size) + console.log('File data:', fileData) + + // Create FormData for upload + const formData = new FormData() + formData.append('file', fileData.file) + formData.append('workflowId', activeWorkflowId) + formData.append('executionId', executionId) + formData.append('workspaceId', workspaceId) + + // Upload the file + const response = await fetch('/api/files/upload', { + method: 'POST', + body: formData, + }) + + if (response.ok) { + const uploadResult = await response.json() + console.log('Upload successful:', uploadResult) + + // Convert upload result to clean UserFile format + const processUploadResult = (result: any) => ({ + id: + result.id || + `file_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`, + name: result.name, + url: result.url, + size: result.size, + type: result.type, + key: result.key, + uploadedAt: result.uploadedAt, + expiresAt: result.expiresAt, + }) + + // The API returns the file directly for single uploads + // or { files: [...] } for multiple uploads + if (uploadResult.files && Array.isArray(uploadResult.files)) { + uploadedFiles.push(...uploadResult.files.map(processUploadResult)) + } else if (uploadResult.path || uploadResult.url) { + // Single file upload - the result IS the file object + uploadedFiles.push(processUploadResult(uploadResult)) + } else { + console.error('Unexpected upload response format:', uploadResult) + } + } else { + const errorText = await response.text() + console.error( + `Failed to upload file ${fileData.name}:`, + response.status, + errorText + ) + } + } + + console.log('All files processed. Uploaded files:', uploadedFiles) + // Update workflow input with uploaded files + workflowInput.files = uploadedFiles + } catch (error) { + console.error('Error uploading files:', error) + // Continue execution even if file upload fails + workflowInput.files = [] + } + } + const onStream = async (streamingExecution: StreamingExecution) => { const promise = (async () => { if (!streamingExecution.stream) return @@ -558,6 +639,9 @@ export function useWorkflowExecution() { selectedOutputIds = chatStore.getState().getSelectedWorkflowOutput(activeWorkflowId) } + // Get workspaceId from workflow metadata + const workspaceId = activeWorkflowId ? workflows[activeWorkflowId]?.workspaceId : undefined + // Create executor options const executorOptions: ExecutorOptions = { workflow, @@ -574,6 +658,7 @@ export function useWorkflowExecution() { })), onStream, executionId, + workspaceId, }, } diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-selector/components/invite-modal/invite-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-selector/components/invite-modal/invite-modal.tsx index 7389896a30..4e47cb20da 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-selector/components/invite-modal/invite-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-selector/components/invite-modal/invite-modal.tsx @@ -18,7 +18,7 @@ import { Input } from '@/components/ui/input' import { Skeleton } from '@/components/ui/skeleton' import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip' import { useSession } from '@/lib/auth-client' -import { validateAndNormalizeEmail } from '@/lib/email/utils' +import { quickValidateEmail } from '@/lib/email/validation' import { createLogger } from '@/lib/logs/console/logger' import type { PermissionType } from '@/lib/permissions/utils' import { cn } from '@/lib/utils' @@ -475,7 +475,9 @@ export function InviteModal({ open, onOpenChange, workspaceName }: InviteModalPr (email: string) => { if (!email.trim()) return false - const { isValid, normalized } = validateAndNormalizeEmail(email) + const normalized = email.trim().toLowerCase() + const validation = quickValidateEmail(normalized) + const isValid = validation.isValid if (emails.includes(normalized) || invalidEmails.includes(normalized)) { return false diff --git a/apps/sim/trigger/webhook-execution.ts b/apps/sim/background/webhook-execution.ts similarity index 96% rename from apps/sim/trigger/webhook-execution.ts rename to apps/sim/background/webhook-execution.ts index e9e106c79f..ed4bc1e185 100644 --- a/apps/sim/trigger/webhook-execution.ts +++ b/apps/sim/background/webhook-execution.ts @@ -204,13 +204,17 @@ export const webhookExecution = task({ } // Create executor and execute - const executor = new Executor( - serializedWorkflow, - processedBlockStates, - decryptedEnvVars, - input || {}, - workflowVariables - ) + const executor = new Executor({ + workflow: serializedWorkflow, + currentBlockStates: processedBlockStates, + envVarValues: decryptedEnvVars, + workflowInput: input || {}, + workflowVariables, + contextExtensions: { + executionId, + workspaceId: '', // TODO: Get from workflow if needed - see comment on line 103 + }, + }) // Set up logging on the executor loggingSession.setupExecutor(executor) diff --git a/apps/sim/trigger/workflow-execution.ts b/apps/sim/background/workflow-execution.ts similarity index 94% rename from apps/sim/trigger/workflow-execution.ts rename to apps/sim/background/workflow-execution.ts index 5a455310c3..a116529022 100644 --- a/apps/sim/trigger/workflow-execution.ts +++ b/apps/sim/background/workflow-execution.ts @@ -126,13 +126,17 @@ export const workflowExecution = task({ ) // Create executor and execute - const executor = new Executor( - serializedWorkflow, - processedBlockStates, - decryptedEnvVars, - payload.input || {}, - {} // workflow variables - ) + const executor = new Executor({ + workflow: serializedWorkflow, + currentBlockStates: processedBlockStates, + envVarValues: decryptedEnvVars, + workflowInput: payload.input || {}, + workflowVariables: {}, + contextExtensions: { + executionId, + workspaceId: '', // TODO: Get from workflow if needed - see comment on line 120 + }, + }) // Set up logging on the executor loggingSession.setupExecutor(executor) diff --git a/apps/sim/blocks/blocks/airtable.ts b/apps/sim/blocks/blocks/airtable.ts index 7810e60e34..f8f0044cbd 100644 --- a/apps/sim/blocks/blocks/airtable.ts +++ b/apps/sim/blocks/blocks/airtable.ts @@ -98,6 +98,15 @@ export const AirtableBlock: BlockConfig = { condition: { field: 'operation', value: 'update' }, required: true, }, + // TRIGGER MODE: Trigger configuration (only shown when trigger mode is active) + { + id: 'triggerConfig', + title: 'Trigger Configuration', + type: 'trigger-config', + layout: 'full', + triggerProvider: 'airtable', + availableTriggers: ['airtable_webhook'], + }, ], tools: { access: [ @@ -176,5 +185,21 @@ export const AirtableBlock: BlockConfig = { records: { type: 'json', description: 'Retrieved record data' }, // Optional: for list, create, updateMultiple record: { type: 'json', description: 'Single record data' }, // Optional: for get, update single metadata: { type: 'json', description: 'Operation metadata' }, // Required: present in all responses + // Trigger outputs + event_type: { type: 'string', description: 'Type of Airtable event' }, + base_id: { type: 'string', description: 'Airtable base identifier' }, + table_id: { type: 'string', description: 'Airtable table identifier' }, + record_id: { type: 'string', description: 'Record identifier that was modified' }, + record_data: { + type: 'string', + description: 'Complete record data (when Include Full Record Data is enabled)', + }, + changed_fields: { type: 'string', description: 'Fields that were changed in the record' }, + webhook_id: { type: 'string', description: 'Unique webhook identifier' }, + timestamp: { type: 'string', description: 'Event timestamp' }, + }, + triggers: { + enabled: true, + available: ['airtable_webhook'], }, } diff --git a/apps/sim/blocks/blocks/api.ts b/apps/sim/blocks/blocks/api.ts index e4a2978784..1cbea104be 100644 --- a/apps/sim/blocks/blocks/api.ts +++ b/apps/sim/blocks/blocks/api.ts @@ -94,8 +94,8 @@ Example: params: { type: 'json', description: 'URL query parameters' }, }, outputs: { - data: { type: 'any', description: 'Response data' }, - status: { type: 'number', description: 'HTTP status code' }, - headers: { type: 'json', description: 'Response headers' }, + data: { type: 'json', description: 'API response data (JSON, text, or other formats)' }, + status: { type: 'number', description: 'HTTP status code (200, 404, 500, etc.)' }, + headers: { type: 'json', description: 'HTTP response headers as key-value pairs' }, }, } diff --git a/apps/sim/blocks/blocks/browser_use.ts b/apps/sim/blocks/blocks/browser_use.ts index 967c80f3d2..af7ad3906b 100644 --- a/apps/sim/blocks/blocks/browser_use.ts +++ b/apps/sim/blocks/blocks/browser_use.ts @@ -71,7 +71,7 @@ export const BrowserUseBlock: BlockConfig = { outputs: { id: { type: 'string', description: 'Task execution identifier' }, success: { type: 'boolean', description: 'Task completion status' }, - output: { type: 'any', description: 'Task output data' }, + output: { type: 'json', description: 'Task output data' }, steps: { type: 'json', description: 'Execution steps taken' }, }, } diff --git a/apps/sim/blocks/blocks/clay.ts b/apps/sim/blocks/blocks/clay.ts index 6272d3da11..7de35c76ac 100644 --- a/apps/sim/blocks/blocks/clay.ts +++ b/apps/sim/blocks/blocks/clay.ts @@ -53,6 +53,6 @@ Plain Text: Best for populating a table in free-form style. data: { type: 'json', description: 'Data to populate' }, }, outputs: { - data: { type: 'any', description: 'Response data' }, + data: { type: 'json', description: 'Response data' }, }, } diff --git a/apps/sim/blocks/blocks/discord.ts b/apps/sim/blocks/blocks/discord.ts index fcc02f008c..39d24bcc17 100644 --- a/apps/sim/blocks/blocks/discord.ts +++ b/apps/sim/blocks/blocks/discord.ts @@ -212,6 +212,20 @@ export const DiscordBlock: BlockConfig = { }, outputs: { message: { type: 'string', description: 'Message content' }, - data: { type: 'any', description: 'Response data' }, + data: { type: 'json', description: 'Response data' }, + // Trigger outputs + content: { type: 'string', description: 'Message content from Discord webhook' }, + username: { type: 'string', description: 'Username of the sender (if provided)' }, + avatar_url: { type: 'string', description: 'Avatar URL of the sender (if provided)' }, + timestamp: { type: 'string', description: 'Timestamp when the webhook was triggered' }, + webhook_id: { type: 'string', description: 'Discord webhook identifier' }, + webhook_token: { type: 'string', description: 'Discord webhook token' }, + guild_id: { type: 'string', description: 'Discord server/guild ID' }, + channel_id: { type: 'string', description: 'Discord channel ID where the event occurred' }, + embeds: { type: 'string', description: 'Embedded content data (if any)' }, + }, + triggers: { + enabled: true, + available: ['discord_webhook'], }, } diff --git a/apps/sim/blocks/blocks/evaluator.ts b/apps/sim/blocks/blocks/evaluator.ts index 692d0c0b16..0f3e05ea81 100644 --- a/apps/sim/blocks/blocks/evaluator.ts +++ b/apps/sim/blocks/blocks/evaluator.ts @@ -315,7 +315,7 @@ export const EvaluatorBlock: BlockConfig = { outputs: { content: { type: 'string', description: 'Evaluation results' }, model: { type: 'string', description: 'Model used' }, - tokens: { type: 'any', description: 'Token usage' }, - cost: { type: 'any', description: 'Cost information' }, + tokens: { type: 'json', description: 'Token usage' }, + cost: { type: 'json', description: 'Cost information' }, } as any, } diff --git a/apps/sim/blocks/blocks/file.ts b/apps/sim/blocks/blocks/file.ts index 7e87be8e69..d56e91dcf5 100644 --- a/apps/sim/blocks/blocks/file.ts +++ b/apps/sim/blocks/blocks/file.ts @@ -111,7 +111,13 @@ export const FileBlock: BlockConfig = { file: { type: 'json', description: 'Uploaded file data' }, }, outputs: { - files: { type: 'json', description: 'Parsed file data' }, - combinedContent: { type: 'string', description: 'Combined file content' }, + files: { + type: 'json', + description: 'Array of parsed file objects with content, metadata, and file properties', + }, + combinedContent: { + type: 'string', + description: 'All file contents merged into a single text string', + }, }, } diff --git a/apps/sim/blocks/blocks/firecrawl.ts b/apps/sim/blocks/blocks/firecrawl.ts index bb9e4427b6..38835d2dc7 100644 --- a/apps/sim/blocks/blocks/firecrawl.ts +++ b/apps/sim/blocks/blocks/firecrawl.ts @@ -121,11 +121,11 @@ export const FirecrawlBlock: BlockConfig = { outputs: { // Scrape output markdown: { type: 'string', description: 'Page content markdown' }, - html: { type: 'any', description: 'Raw HTML content' }, + html: { type: 'string', description: 'Raw HTML content' }, metadata: { type: 'json', description: 'Page metadata' }, // Search output data: { type: 'json', description: 'Search results data' }, - warning: { type: 'any', description: 'Warning messages' }, + warning: { type: 'string', description: 'Warning messages' }, // Crawl output pages: { type: 'json', description: 'Crawled pages data' }, total: { type: 'number', description: 'Total pages found' }, diff --git a/apps/sim/blocks/blocks/function.ts b/apps/sim/blocks/blocks/function.ts index 5000424618..b2de04dae1 100644 --- a/apps/sim/blocks/blocks/function.ts +++ b/apps/sim/blocks/blocks/function.ts @@ -80,7 +80,10 @@ try { timeout: { type: 'number', description: 'Execution timeout' }, }, outputs: { - result: { type: 'any', description: 'Execution result' }, - stdout: { type: 'string', description: 'Console output' }, + result: { type: 'json', description: 'Return value from the executed JavaScript function' }, + stdout: { + type: 'string', + description: 'Console log output and debug messages from function execution', + }, }, } diff --git a/apps/sim/blocks/blocks/generic_webhook.ts b/apps/sim/blocks/blocks/generic_webhook.ts new file mode 100644 index 0000000000..2b392b2284 --- /dev/null +++ b/apps/sim/blocks/blocks/generic_webhook.ts @@ -0,0 +1,47 @@ +import { WebhookIcon } from '@/components/icons' +import type { BlockConfig } from '@/blocks/types' + +export const GenericWebhookBlock: BlockConfig = { + type: 'generic_webhook', + name: 'Webhook', + description: 'Receive webhooks from any service', + category: 'triggers', + icon: WebhookIcon, + bgColor: '#10B981', // Green color for triggers + + subBlocks: [ + // Generic webhook configuration - always visible + { + id: 'triggerConfig', + title: 'Webhook Configuration', + type: 'trigger-config', + layout: 'full', + triggerProvider: 'generic', + availableTriggers: ['generic_webhook'], + }, + ], + + tools: { + access: [], // No external tools needed for triggers + }, + + inputs: {}, // No inputs - webhook triggers receive data externally + + outputs: { + // Generic webhook outputs that can be used with any webhook payload + payload: { type: 'json', description: 'Complete webhook payload' }, + headers: { type: 'json', description: 'Request headers' }, + method: { type: 'string', description: 'HTTP method' }, + url: { type: 'string', description: 'Request URL' }, + timestamp: { type: 'string', description: 'Webhook received timestamp' }, + // Common webhook fields that services often use + event: { type: 'string', description: 'Event type from payload' }, + id: { type: 'string', description: 'Event ID from payload' }, + data: { type: 'json', description: 'Event data from payload' }, + }, + + triggers: { + enabled: true, + available: ['generic_webhook'], + }, +} diff --git a/apps/sim/blocks/blocks/github.ts b/apps/sim/blocks/blocks/github.ts index 8900d8a6af..52f35d884e 100644 --- a/apps/sim/blocks/blocks/github.ts +++ b/apps/sim/blocks/blocks/github.ts @@ -5,9 +5,9 @@ import type { GitHubResponse } from '@/tools/github/types' export const GitHubBlock: BlockConfig = { type: 'github', name: 'GitHub', - description: 'Interact with GitHub', + description: 'Interact with GitHub or trigger workflows from GitHub events', longDescription: - 'Access GitHub repositories, pull requests, and comments through the GitHub API. Automate code reviews, PR management, and repository interactions within your workflow.', + 'Access GitHub repositories, pull requests, and comments through the GitHub API. Automate code reviews, PR management, and repository interactions within your workflow. Trigger workflows from GitHub events like push, pull requests, and issues.', docsLink: 'https://docs.sim.ai/tools/github', category: 'tools', bgColor: '#181C1E', @@ -86,6 +86,15 @@ export const GitHubBlock: BlockConfig = { password: true, required: true, }, + // TRIGGER MODE: Trigger configuration (only shown when trigger mode is active) + { + id: 'triggerConfig', + title: 'Trigger Configuration', + type: 'trigger-config', + layout: 'full', + triggerProvider: 'github', + availableTriggers: ['github_webhook'], + }, { id: 'commentType', title: 'Comment Type', @@ -164,5 +173,27 @@ export const GitHubBlock: BlockConfig = { outputs: { content: { type: 'string', description: 'Response content' }, metadata: { type: 'json', description: 'Response metadata' }, + // Trigger outputs + action: { type: 'string', description: 'The action that was performed' }, + event_type: { type: 'string', description: 'Type of GitHub event' }, + repository: { type: 'string', description: 'Repository full name' }, + repository_name: { type: 'string', description: 'Repository name only' }, + repository_owner: { type: 'string', description: 'Repository owner username' }, + sender: { type: 'string', description: 'Username of the user who triggered the event' }, + sender_id: { type: 'string', description: 'User ID of the sender' }, + ref: { type: 'string', description: 'Git reference (for push events)' }, + before: { type: 'string', description: 'SHA of the commit before the push' }, + after: { type: 'string', description: 'SHA of the commit after the push' }, + commits: { type: 'string', description: 'Array of commit objects (for push events)' }, + pull_request: { type: 'string', description: 'Pull request object (for pull_request events)' }, + issue: { type: 'string', description: 'Issue object (for issues events)' }, + comment: { type: 'string', description: 'Comment object (for comment events)' }, + branch: { type: 'string', description: 'Branch name extracted from ref' }, + commit_message: { type: 'string', description: 'Latest commit message' }, + commit_author: { type: 'string', description: 'Author of the latest commit' }, + }, + triggers: { + enabled: true, + available: ['github_webhook'], }, } diff --git a/apps/sim/blocks/blocks/gmail.ts b/apps/sim/blocks/blocks/gmail.ts index ab0db99d30..436404040b 100644 --- a/apps/sim/blocks/blocks/gmail.ts +++ b/apps/sim/blocks/blocks/gmail.ts @@ -5,9 +5,9 @@ import type { GmailToolResponse } from '@/tools/gmail/types' export const GmailBlock: BlockConfig = { type: 'gmail', name: 'Gmail', - description: 'Send Gmail', + description: 'Send Gmail or trigger workflows from Gmail events', longDescription: - 'Integrate Gmail functionality to send email messages within your workflow. Automate email communications and process email content using OAuth authentication.', + 'Comprehensive Gmail integration with OAuth authentication. Send email messages, read email content, and trigger workflows from Gmail events like new emails and label changes.', docsLink: 'https://docs.sim.ai/tools/gmail', category: 'tools', bgColor: '#E0E0E0', @@ -38,7 +38,7 @@ export const GmailBlock: BlockConfig = { requiredScopes: [ 'https://www.googleapis.com/auth/gmail.send', 'https://www.googleapis.com/auth/gmail.modify', - // 'https://www.googleapis.com/auth/gmail.readonly', + 'https://www.googleapis.com/auth/gmail.readonly', 'https://www.googleapis.com/auth/gmail.labels', ], placeholder: 'Select Gmail account', @@ -72,6 +72,27 @@ export const GmailBlock: BlockConfig = { condition: { field: 'operation', value: ['send_gmail', 'draft_gmail'] }, required: true, }, + // Advanced Settings - Additional Recipients + { + id: 'cc', + title: 'CC', + type: 'short-input', + layout: 'full', + placeholder: 'CC recipients (comma-separated)', + condition: { field: 'operation', value: ['send_gmail', 'draft_gmail'] }, + mode: 'advanced', + required: false, + }, + { + id: 'bcc', + title: 'BCC', + type: 'short-input', + layout: 'full', + placeholder: 'BCC recipients (comma-separated)', + condition: { field: 'operation', value: ['send_gmail', 'draft_gmail'] }, + mode: 'advanced', + required: false, + }, // Label/folder selector (basic mode) { id: 'folder', @@ -105,6 +126,13 @@ export const GmailBlock: BlockConfig = { layout: 'full', condition: { field: 'operation', value: 'read_gmail' }, }, + { + id: 'includeAttachments', + title: 'Include Attachments', + type: 'switch', + layout: 'full', + condition: { field: 'operation', value: 'read_gmail' }, + }, { id: 'messageId', title: 'Message ID', @@ -138,6 +166,15 @@ export const GmailBlock: BlockConfig = { placeholder: 'Maximum number of results (default: 10)', condition: { field: 'operation', value: ['search_gmail', 'read_gmail'] }, }, + // TRIGGER MODE: Trigger configuration (only shown when trigger mode is active) + { + id: 'triggerConfig', + title: 'Trigger Configuration', + type: 'trigger-config', + layout: 'full', + triggerProvider: 'gmail', + availableTriggers: ['gmail_poller'], + }, ], tools: { access: ['gmail_send', 'gmail_draft', 'gmail_read', 'gmail_search'], @@ -182,17 +219,40 @@ export const GmailBlock: BlockConfig = { to: { type: 'string', description: 'Recipient email address' }, subject: { type: 'string', description: 'Email subject' }, body: { type: 'string', description: 'Email content' }, + cc: { type: 'string', description: 'CC recipients (comma-separated)' }, + bcc: { type: 'string', description: 'BCC recipients (comma-separated)' }, // Read operation inputs folder: { type: 'string', description: 'Gmail folder' }, manualFolder: { type: 'string', description: 'Manual folder name' }, messageId: { type: 'string', description: 'Message identifier' }, unreadOnly: { type: 'boolean', description: 'Unread messages only' }, + includeAttachments: { type: 'boolean', description: 'Include email attachments' }, // Search operation inputs query: { type: 'string', description: 'Search query' }, maxResults: { type: 'number', description: 'Maximum results' }, }, outputs: { + // Tool outputs content: { type: 'string', description: 'Response content' }, metadata: { type: 'json', description: 'Email metadata' }, + attachments: { type: 'json', description: 'Email attachments array' }, + // Trigger outputs + email_id: { type: 'string', description: 'Gmail message ID' }, + thread_id: { type: 'string', description: 'Gmail thread ID' }, + subject: { type: 'string', description: 'Email subject line' }, + from: { type: 'string', description: 'Sender email address' }, + to: { type: 'string', description: 'Recipient email address' }, + cc: { type: 'string', description: 'CC recipients (comma-separated)' }, + date: { type: 'string', description: 'Email date in ISO format' }, + body_text: { type: 'string', description: 'Plain text email body' }, + body_html: { type: 'string', description: 'HTML email body' }, + labels: { type: 'string', description: 'Email labels (comma-separated)' }, + has_attachments: { type: 'boolean', description: 'Whether email has attachments' }, + raw_email: { type: 'json', description: 'Complete raw email data from Gmail API (if enabled)' }, + timestamp: { type: 'string', description: 'Event timestamp' }, + }, + triggers: { + enabled: true, + available: ['gmail_poller'], }, } diff --git a/apps/sim/blocks/blocks/jira.ts b/apps/sim/blocks/blocks/jira.ts index 399ed00ee4..10ff0a479e 100644 --- a/apps/sim/blocks/blocks/jira.ts +++ b/apps/sim/blocks/blocks/jira.ts @@ -240,13 +240,24 @@ export const JiraBlock: BlockConfig = { issueType: { type: 'string', description: 'Issue type' }, }, outputs: { - ts: { type: 'string', description: 'Timestamp' }, - issueKey: { type: 'string', description: 'Issue key' }, - summary: { type: 'string', description: 'Issue summary' }, - description: { type: 'string', description: 'Issue description' }, - created: { type: 'string', description: 'Creation date' }, - updated: { type: 'string', description: 'Update date' }, - success: { type: 'boolean', description: 'Operation success' }, - url: { type: 'string', description: 'Issue URL' }, + // Common outputs across all Jira operations + ts: { type: 'string', description: 'Timestamp of the operation' }, + + // jira_retrieve (read) outputs + issueKey: { type: 'string', description: 'Issue key (e.g., PROJ-123)' }, + summary: { type: 'string', description: 'Issue summary/title' }, + description: { type: 'string', description: 'Issue description content' }, + created: { type: 'string', description: 'Issue creation date' }, + updated: { type: 'string', description: 'Issue last update date' }, + + // jira_update outputs + success: { type: 'boolean', description: 'Whether the update operation was successful' }, + + // jira_write (create) outputs + url: { type: 'string', description: 'URL to the created/accessed issue' }, + + // jira_bulk_read outputs (array of issues) + // Note: bulk_read returns an array in the output field, each item contains: + // ts, summary, description, created, updated }, } diff --git a/apps/sim/blocks/blocks/mem0.ts b/apps/sim/blocks/blocks/mem0.ts index 5822f593df..47ab17ea13 100644 --- a/apps/sim/blocks/blocks/mem0.ts +++ b/apps/sim/blocks/blocks/mem0.ts @@ -295,8 +295,8 @@ export const Mem0Block: BlockConfig = { limit: { type: 'number', description: 'Result limit' }, }, outputs: { - ids: { type: 'any', description: 'Memory identifiers' }, - memories: { type: 'any', description: 'Memory data' }, - searchResults: { type: 'any', description: 'Search results' }, + ids: { type: 'json', description: 'Memory identifiers' }, + memories: { type: 'json', description: 'Memory data' }, + searchResults: { type: 'json', description: 'Search results' }, }, } diff --git a/apps/sim/blocks/blocks/memory.ts b/apps/sim/blocks/blocks/memory.ts index f4c478169f..e2dd0969b5 100644 --- a/apps/sim/blocks/blocks/memory.ts +++ b/apps/sim/blocks/blocks/memory.ts @@ -186,7 +186,7 @@ export const MemoryBlock: BlockConfig = { content: { type: 'string', description: 'Memory content' }, }, outputs: { - memories: { type: 'any', description: 'Memory data' }, + memories: { type: 'json', description: 'Memory data' }, id: { type: 'string', description: 'Memory identifier' }, }, } diff --git a/apps/sim/blocks/blocks/microsoft_excel.ts b/apps/sim/blocks/blocks/microsoft_excel.ts index 56c6fd553c..bed00152a9 100644 --- a/apps/sim/blocks/blocks/microsoft_excel.ts +++ b/apps/sim/blocks/blocks/microsoft_excel.ts @@ -197,13 +197,19 @@ export const MicrosoftExcelBlock: BlockConfig = { valueInputOption: { type: 'string', description: 'Value input option' }, }, outputs: { - data: { type: 'json', description: 'Sheet data' }, - metadata: { type: 'json', description: 'Operation metadata' }, - updatedRange: { type: 'string', description: 'Updated range' }, - updatedRows: { type: 'number', description: 'Updated rows count' }, - updatedColumns: { type: 'number', description: 'Updated columns count' }, - updatedCells: { type: 'number', description: 'Updated cells count' }, - index: { type: 'number', description: 'Row index' }, - values: { type: 'json', description: 'Table values' }, + data: { type: 'json', description: 'Excel range data with sheet information and cell values' }, + metadata: { + type: 'json', + description: 'Spreadsheet metadata including ID, URL, and sheet details', + }, + updatedRange: { type: 'string', description: 'The range that was updated (write operations)' }, + updatedRows: { type: 'number', description: 'Number of rows updated (write operations)' }, + updatedColumns: { type: 'number', description: 'Number of columns updated (write operations)' }, + updatedCells: { + type: 'number', + description: 'Total number of cells updated (write operations)', + }, + index: { type: 'number', description: 'Row index for table add operations' }, + values: { type: 'json', description: 'Cell values array for table add operations' }, }, } diff --git a/apps/sim/blocks/blocks/microsoft_teams.ts b/apps/sim/blocks/blocks/microsoft_teams.ts index d3cfb1861a..97818c2213 100644 --- a/apps/sim/blocks/blocks/microsoft_teams.ts +++ b/apps/sim/blocks/blocks/microsoft_teams.ts @@ -221,8 +221,42 @@ export const MicrosoftTeamsBlock: BlockConfig = { content: { type: 'string', description: 'Message content' }, }, outputs: { - content: { type: 'string', description: 'Message content' }, - metadata: { type: 'json', description: 'Message metadata' }, - updatedContent: { type: 'boolean', description: 'Content update status' }, + // Read operation outputs + content: { type: 'string', description: 'Formatted message content from chat/channel' }, + metadata: { type: 'json', description: 'Message metadata with full details' }, + messageCount: { type: 'number', description: 'Number of messages retrieved' }, + messages: { type: 'json', description: 'Array of message objects' }, + totalAttachments: { type: 'number', description: 'Total number of attachments' }, + attachmentTypes: { type: 'json', description: 'Array of attachment content types' }, + // Write operation outputs + updatedContent: { + type: 'boolean', + description: 'Whether content was successfully updated/sent', + }, + messageId: { type: 'string', description: 'ID of the created/sent message' }, + createdTime: { type: 'string', description: 'Timestamp when message was created' }, + url: { type: 'string', description: 'Web URL to the message' }, + // Individual message fields (from read operations) + sender: { type: 'string', description: 'Message sender display name' }, + messageTimestamp: { type: 'string', description: 'Individual message timestamp' }, + messageType: { + type: 'string', + description: 'Type of message (message, systemEventMessage, etc.)', + }, + // Trigger outputs + type: { type: 'string', description: 'Type of Teams message' }, + id: { type: 'string', description: 'Unique message identifier' }, + timestamp: { type: 'string', description: 'Message timestamp' }, + localTimestamp: { type: 'string', description: 'Local timestamp of the message' }, + serviceUrl: { type: 'string', description: 'Microsoft Teams service URL' }, + channelId: { type: 'string', description: 'Teams channel ID where the event occurred' }, + from_id: { type: 'string', description: 'User ID who sent the message' }, + from_name: { type: 'string', description: 'Username who sent the message' }, + conversation_id: { type: 'string', description: 'Conversation/thread ID' }, + text: { type: 'string', description: 'Message text content' }, + }, + triggers: { + enabled: true, + available: ['microsoftteams_webhook'], }, } diff --git a/apps/sim/blocks/blocks/notion.ts b/apps/sim/blocks/blocks/notion.ts index ab102cad98..2ecc1c2045 100644 --- a/apps/sim/blocks/blocks/notion.ts +++ b/apps/sim/blocks/blocks/notion.ts @@ -308,7 +308,17 @@ export const NotionBlock: BlockConfig = { filterType: { type: 'string', description: 'Filter type' }, }, outputs: { - content: { type: 'string', description: 'Page content' }, - metadata: { type: 'any', description: 'Page metadata' }, + // Common outputs across all Notion operations + content: { + type: 'string', + description: 'Page content, search results, or confirmation messages', + }, + + // Metadata object containing operation-specific information + metadata: { + type: 'json', + description: + 'Metadata containing operation-specific details including page/database info, results, and pagination data', + }, }, } diff --git a/apps/sim/blocks/blocks/outlook.ts b/apps/sim/blocks/blocks/outlook.ts index b871ccfbfa..e8bef11e5e 100644 --- a/apps/sim/blocks/blocks/outlook.ts +++ b/apps/sim/blocks/blocks/outlook.ts @@ -145,6 +145,15 @@ export const OutlookBlock: BlockConfig = { placeholder: 'Number of emails to retrieve (default: 1, max: 10)', condition: { field: 'operation', value: 'read_outlook' }, }, + // TRIGGER MODE: Trigger configuration (only shown when trigger mode is active) + { + id: 'triggerConfig', + title: 'Trigger Configuration', + type: 'trigger-config', + layout: 'full', + triggerProvider: 'outlook', + availableTriggers: ['outlook_poller'], + }, ], tools: { access: ['outlook_send', 'outlook_draft', 'outlook_read'], @@ -193,7 +202,36 @@ export const OutlookBlock: BlockConfig = { maxResults: { type: 'number', description: 'Maximum emails' }, }, outputs: { + // Common outputs message: { type: 'string', description: 'Response message' }, - results: { type: 'json', description: 'Email results' }, + results: { type: 'json', description: 'Operation results' }, + // Send operation specific outputs + status: { type: 'string', description: 'Email send status (sent)' }, + timestamp: { type: 'string', description: 'Operation timestamp' }, + // Draft operation specific outputs + messageId: { type: 'string', description: 'Draft message ID' }, + subject: { type: 'string', description: 'Draft email subject' }, + // Read operation specific outputs + emailCount: { type: 'number', description: 'Number of emails retrieved' }, + emails: { type: 'json', description: 'Array of email objects' }, + emailId: { type: 'string', description: 'Individual email ID' }, + emailSubject: { type: 'string', description: 'Individual email subject' }, + bodyPreview: { type: 'string', description: 'Email body preview' }, + bodyContent: { type: 'string', description: 'Full email body content' }, + sender: { type: 'json', description: 'Email sender information' }, + from: { type: 'json', description: 'Email from information' }, + recipients: { type: 'json', description: 'Email recipients' }, + receivedDateTime: { type: 'string', description: 'Email received timestamp' }, + sentDateTime: { type: 'string', description: 'Email sent timestamp' }, + hasAttachments: { type: 'boolean', description: 'Whether email has attachments' }, + isRead: { type: 'boolean', description: 'Whether email is read' }, + importance: { type: 'string', description: 'Email importance level' }, + // Trigger outputs + email: { type: 'json', description: 'Email data from trigger' }, + rawEmail: { type: 'json', description: 'Complete raw email data from Microsoft Graph API' }, + }, + triggers: { + enabled: true, + available: ['outlook_poller'], }, } diff --git a/apps/sim/blocks/blocks/pinecone.ts b/apps/sim/blocks/blocks/pinecone.ts index 6d85eefb35..3d8d5db250 100644 --- a/apps/sim/blocks/blocks/pinecone.ts +++ b/apps/sim/blocks/blocks/pinecone.ts @@ -283,11 +283,11 @@ export const PineconeBlock: BlockConfig = { }, outputs: { - matches: { type: 'any', description: 'Search matches' }, - upsertedCount: { type: 'any', description: 'Upserted count' }, - data: { type: 'any', description: 'Response data' }, - model: { type: 'any', description: 'Model information' }, - vector_type: { type: 'any', description: 'Vector type' }, - usage: { type: 'any', description: 'Usage statistics' }, + matches: { type: 'json', description: 'Search matches' }, + upsertedCount: { type: 'number', description: 'Upserted count' }, + data: { type: 'json', description: 'Response data' }, + model: { type: 'string', description: 'Model information' }, + vector_type: { type: 'string', description: 'Vector type' }, + usage: { type: 'json', description: 'Usage statistics' }, }, } diff --git a/apps/sim/blocks/blocks/qdrant.ts b/apps/sim/blocks/blocks/qdrant.ts index 1dbc6e11bd..c1fe14cfab 100644 --- a/apps/sim/blocks/blocks/qdrant.ts +++ b/apps/sim/blocks/blocks/qdrant.ts @@ -198,9 +198,9 @@ export const QdrantBlock: BlockConfig = { }, outputs: { - matches: { type: 'any', description: 'Search matches' }, - upsertedCount: { type: 'any', description: 'Upserted count' }, - data: { type: 'any', description: 'Response data' }, - status: { type: 'any', description: 'Operation status' }, + matches: { type: 'json', description: 'Search matches' }, + upsertedCount: { type: 'number', description: 'Upserted count' }, + data: { type: 'json', description: 'Response data' }, + status: { type: 'string', description: 'Operation status' }, }, } diff --git a/apps/sim/blocks/blocks/router.ts b/apps/sim/blocks/blocks/router.ts index 97fa9d9d5d..825dec5a03 100644 --- a/apps/sim/blocks/blocks/router.ts +++ b/apps/sim/blocks/blocks/router.ts @@ -188,8 +188,8 @@ export const RouterBlock: BlockConfig = { outputs: { content: { type: 'string', description: 'Routing response content' }, model: { type: 'string', description: 'Model used' }, - tokens: { type: 'any', description: 'Token usage' }, - cost: { type: 'any', description: 'Cost information' }, + tokens: { type: 'json', description: 'Token usage' }, + cost: { type: 'json', description: 'Cost information' }, selectedPath: { type: 'json', description: 'Selected routing path' }, }, } diff --git a/apps/sim/blocks/blocks/slack.ts b/apps/sim/blocks/blocks/slack.ts index a116231395..8c11694614 100644 --- a/apps/sim/blocks/blocks/slack.ts +++ b/apps/sim/blocks/blocks/slack.ts @@ -5,9 +5,9 @@ import type { SlackResponse } from '@/tools/slack/types' export const SlackBlock: BlockConfig = { type: 'slack', name: 'Slack', - description: 'Send messages to Slack', + description: 'Send messages to Slack or trigger workflows from Slack events', longDescription: - "Comprehensive Slack integration with OAuth authentication. Send formatted messages using Slack's mrkdwn syntax.", + "Comprehensive Slack integration with OAuth authentication. Send formatted messages using Slack's mrkdwn syntax or trigger workflows from Slack events like mentions and messages.", docsLink: 'https://docs.sim.ai/tools/slack', category: 'tools', bgColor: '#611f69', @@ -151,6 +151,15 @@ export const SlackBlock: BlockConfig = { value: 'read', }, }, + // TRIGGER MODE: Trigger configuration (only shown when trigger mode is active) + { + id: 'triggerConfig', + title: 'Trigger Configuration', + type: 'trigger-config', + layout: 'full', + triggerProvider: 'slack', + availableTriggers: ['slack_webhook'], + }, ], tools: { access: ['slack_message', 'slack_canvas', 'slack_message_reader'], @@ -257,10 +266,30 @@ export const SlackBlock: BlockConfig = { oldest: { type: 'string', description: 'Oldest timestamp' }, }, outputs: { - ts: { type: 'string', description: 'Message timestamp' }, - channel: { type: 'string', description: 'Channel identifier' }, - canvas_id: { type: 'string', description: 'Canvas identifier' }, + // slack_message outputs + ts: { type: 'string', description: 'Message timestamp returned by Slack API' }, + channel: { type: 'string', description: 'Channel identifier where message was sent' }, + + // slack_canvas outputs + canvas_id: { type: 'string', description: 'Canvas identifier for created canvases' }, title: { type: 'string', description: 'Canvas title' }, - messages: { type: 'json', description: 'Message data' }, + + // slack_message_reader outputs + messages: { + type: 'json', + description: 'Array of message objects', + }, + + // Trigger outputs (when used as webhook trigger) + event_type: { type: 'string', description: 'Type of Slack event that triggered the workflow' }, + channel_name: { type: 'string', description: 'Human-readable channel name' }, + user_name: { type: 'string', description: 'Username who triggered the event' }, + team_id: { type: 'string', description: 'Slack workspace/team ID' }, + event_id: { type: 'string', description: 'Unique event identifier for the trigger' }, + }, + // New: Trigger capabilities + triggers: { + enabled: true, + available: ['slack_webhook'], }, } diff --git a/apps/sim/blocks/blocks/stagehand_agent.ts b/apps/sim/blocks/blocks/stagehand_agent.ts index f638b659fd..225ec638f0 100644 --- a/apps/sim/blocks/blocks/stagehand_agent.ts +++ b/apps/sim/blocks/blocks/stagehand_agent.ts @@ -71,6 +71,6 @@ export const StagehandAgentBlock: BlockConfig = { }, outputs: { agentResult: { type: 'json', description: 'Agent execution result' }, - structuredOutput: { type: 'any', description: 'Structured output data' }, + structuredOutput: { type: 'json', description: 'Structured output data' }, }, } diff --git a/apps/sim/blocks/blocks/supabase.ts b/apps/sim/blocks/blocks/supabase.ts index eb22dc6789..3acd1ae1b8 100644 --- a/apps/sim/blocks/blocks/supabase.ts +++ b/apps/sim/blocks/blocks/supabase.ts @@ -206,7 +206,13 @@ export const SupabaseBlock: BlockConfig = { limit: { type: 'number', description: 'Result limit' }, }, outputs: { - message: { type: 'string', description: 'Operation message' }, - results: { type: 'json', description: 'Query results' }, + message: { + type: 'string', + description: 'Success or error message describing the operation outcome', + }, + results: { + type: 'json', + description: 'Database records returned from query, insert, update, or delete operations', + }, }, } diff --git a/apps/sim/blocks/blocks/tavily.ts b/apps/sim/blocks/blocks/tavily.ts index f07d550ca9..04684372e3 100644 --- a/apps/sim/blocks/blocks/tavily.ts +++ b/apps/sim/blocks/blocks/tavily.ts @@ -97,7 +97,7 @@ export const TavilyBlock: BlockConfig = { }, outputs: { results: { type: 'json', description: 'Search results data' }, - answer: { type: 'any', description: 'Search answer' }, + answer: { type: 'string', description: 'Search answer' }, query: { type: 'string', description: 'Query used' }, content: { type: 'string', description: 'Extracted content' }, title: { type: 'string', description: 'Page title' }, diff --git a/apps/sim/blocks/blocks/telegram.ts b/apps/sim/blocks/blocks/telegram.ts index 3c67e6f621..eec1244b26 100644 --- a/apps/sim/blocks/blocks/telegram.ts +++ b/apps/sim/blocks/blocks/telegram.ts @@ -5,9 +5,9 @@ import type { TelegramMessageResponse } from '@/tools/telegram/types' export const TelegramBlock: BlockConfig = { type: 'telegram', name: 'Telegram', - description: 'Send a message through Telegram', + description: 'Send messages through Telegram or trigger workflows from Telegram events', longDescription: - 'Send messages to any Telegram channel using your Bot API key. Integrate automated notifications and alerts into your workflow to keep your team informed.', + 'Send messages to any Telegram channel using your Bot API key or trigger workflows from Telegram bot messages. Integrate automated notifications and alerts into your workflow to keep your team informed.', docsLink: 'https://docs.sim.ai/tools/telegram', category: 'tools', bgColor: '#E0E0E0', @@ -48,6 +48,15 @@ export const TelegramBlock: BlockConfig = { placeholder: 'Enter the message to send', required: true, }, + // TRIGGER MODE: Trigger configuration (only shown when trigger mode is active) + { + id: 'triggerConfig', + title: 'Trigger Configuration', + type: 'trigger-config', + layout: 'full', + triggerProvider: 'telegram', + availableTriggers: ['telegram_webhook'], + }, ], tools: { access: ['telegram_message'], @@ -58,7 +67,38 @@ export const TelegramBlock: BlockConfig = { text: { type: 'string', description: 'Message text' }, }, outputs: { - ok: { type: 'boolean', description: 'Success status' }, - result: { type: 'json', description: 'Message result' }, + // Send message operation outputs + ok: { type: 'boolean', description: 'API response success status' }, + result: { type: 'json', description: 'Complete message result object from Telegram API' }, + // Specific result fields + messageId: { type: 'number', description: 'Sent message ID' }, + chatId: { type: 'number', description: 'Chat ID where message was sent' }, + chatType: { type: 'string', description: 'Type of chat (private, group, supergroup, channel)' }, + username: { type: 'string', description: 'Chat username (if available)' }, + messageDate: { type: 'number', description: 'Unix timestamp of sent message' }, + messageText: { type: 'string', description: 'Text content of sent message' }, + // Webhook trigger outputs (incoming messages) + update_id: { type: 'number', description: 'Unique identifier for the update' }, + message_id: { type: 'number', description: 'Unique message identifier from webhook' }, + from_id: { type: 'number', description: 'User ID who sent the message' }, + from_username: { type: 'string', description: 'Username of the sender' }, + from_first_name: { type: 'string', description: 'First name of the sender' }, + from_last_name: { type: 'string', description: 'Last name of the sender' }, + chat_id: { type: 'number', description: 'Unique identifier for the chat' }, + chat_type: { + type: 'string', + description: 'Type of chat (private, group, supergroup, channel)', + }, + chat_title: { type: 'string', description: 'Title of the chat (for groups and channels)' }, + text: { type: 'string', description: 'Message text content from webhook' }, + date: { type: 'number', description: 'Date the message was sent (Unix timestamp)' }, + entities: { + type: 'json', + description: 'Special entities in the message (mentions, hashtags, etc.)', + }, + }, + triggers: { + enabled: true, + available: ['telegram_webhook'], }, } diff --git a/apps/sim/blocks/blocks/translate.ts b/apps/sim/blocks/blocks/translate.ts index a0f4460465..1fb761b71f 100644 --- a/apps/sim/blocks/blocks/translate.ts +++ b/apps/sim/blocks/blocks/translate.ts @@ -99,6 +99,6 @@ export const TranslateBlock: BlockConfig = { outputs: { content: { type: 'string', description: 'Translated text' }, model: { type: 'string', description: 'Model used' }, - tokens: { type: 'any', description: 'Token usage' }, + tokens: { type: 'json', description: 'Token usage' }, }, } diff --git a/apps/sim/blocks/blocks/twilio.ts b/apps/sim/blocks/blocks/twilio.ts index c47c259286..e25f495919 100644 --- a/apps/sim/blocks/blocks/twilio.ts +++ b/apps/sim/blocks/blocks/twilio.ts @@ -68,8 +68,8 @@ export const TwilioSMSBlock: BlockConfig = { }, outputs: { success: { type: 'boolean', description: 'Send success status' }, - messageId: { type: 'any', description: 'Message identifier' }, - status: { type: 'any', description: 'Delivery status' }, - error: { type: 'any', description: 'Error information' }, + messageId: { type: 'string', description: 'Twilio message SID' }, + status: { type: 'string', description: 'SMS delivery status (queued, sent, delivered, etc.)' }, + error: { type: 'string', description: 'Error information if sending fails' }, }, } diff --git a/apps/sim/blocks/blocks/vision.ts b/apps/sim/blocks/blocks/vision.ts index 2dc9c29b5b..3ac23c16a5 100644 --- a/apps/sim/blocks/blocks/vision.ts +++ b/apps/sim/blocks/blocks/vision.ts @@ -62,7 +62,7 @@ export const VisionBlock: BlockConfig = { }, outputs: { content: { type: 'string', description: 'Analysis result' }, - model: { type: 'any', description: 'Model used' }, - tokens: { type: 'any', description: 'Token usage' }, + model: { type: 'string', description: 'Model used' }, + tokens: { type: 'number', description: 'Token usage' }, }, } diff --git a/apps/sim/blocks/blocks/wealthbox.ts b/apps/sim/blocks/blocks/wealthbox.ts index f50a3b2486..fdfbc34cde 100644 --- a/apps/sim/blocks/blocks/wealthbox.ts +++ b/apps/sim/blocks/blocks/wealthbox.ts @@ -255,13 +255,28 @@ export const WealthboxBlock: BlockConfig = { dueDate: { type: 'string', description: 'Due date' }, }, outputs: { - note: { type: 'any', description: 'Note data' }, - notes: { type: 'any', description: 'Notes list' }, - contact: { type: 'any', description: 'Contact data' }, - contacts: { type: 'any', description: 'Contacts list' }, - task: { type: 'any', description: 'Task data' }, - tasks: { type: 'any', description: 'Tasks list' }, - metadata: { type: 'json', description: 'Operation metadata' }, - success: { type: 'any', description: 'Success status' }, + note: { + type: 'json', + description: 'Single note object with ID, content, creator, and linked contacts', + }, + notes: { type: 'json', description: 'Array of note objects from bulk read operations' }, + contact: { + type: 'json', + description: 'Single contact object with name, email, phone, and background info', + }, + contacts: { type: 'json', description: 'Array of contact objects from bulk read operations' }, + task: { + type: 'json', + description: 'Single task object with name, due date, description, and priority', + }, + tasks: { type: 'json', description: 'Array of task objects from bulk read operations' }, + metadata: { + type: 'json', + description: 'Operation metadata including item IDs, types, and operation details', + }, + success: { + type: 'boolean', + description: 'Boolean indicating whether the operation completed successfully', + }, }, } diff --git a/apps/sim/blocks/blocks/webhook.ts b/apps/sim/blocks/blocks/webhook.ts index 96292ca666..d5b870fa00 100644 --- a/apps/sim/blocks/blocks/webhook.ts +++ b/apps/sim/blocks/blocks/webhook.ts @@ -39,6 +39,7 @@ export const WebhookBlock: BlockConfig = { category: 'triggers', icon: WebhookIcon, bgColor: '#10B981', // Green color for triggers + hideFromToolbar: true, // Hidden for backwards compatibility - use generic webhook trigger instead subBlocks: [ { diff --git a/apps/sim/blocks/blocks/whatsapp.ts b/apps/sim/blocks/blocks/whatsapp.ts index 6d62f13af9..dde2c93c81 100644 --- a/apps/sim/blocks/blocks/whatsapp.ts +++ b/apps/sim/blocks/blocks/whatsapp.ts @@ -60,8 +60,19 @@ export const WhatsAppBlock: BlockConfig = { accessToken: { type: 'string', description: 'WhatsApp access token' }, }, outputs: { + // Send operation outputs success: { type: 'boolean', description: 'Send success status' }, - messageId: { type: 'any', description: 'Message identifier' }, - error: { type: 'any', description: 'Error information' }, + messageId: { type: 'string', description: 'WhatsApp message identifier' }, + error: { type: 'string', description: 'Error information if sending fails' }, + // Webhook trigger outputs + from: { type: 'string', description: 'Sender phone number' }, + to: { type: 'string', description: 'Recipient phone number' }, + text: { type: 'string', description: 'Message text content' }, + timestamp: { type: 'string', description: 'Message timestamp' }, + type: { type: 'string', description: 'Message type (text, image, etc.)' }, + }, + triggers: { + enabled: true, + available: ['whatsapp_webhook'], }, } diff --git a/apps/sim/blocks/blocks/x.ts b/apps/sim/blocks/blocks/x.ts index 03ccba5d89..66667a8e63 100644 --- a/apps/sim/blocks/blocks/x.ts +++ b/apps/sim/blocks/blocks/x.ts @@ -210,12 +210,12 @@ export const XBlock: BlockConfig = { }, outputs: { tweet: { type: 'json', description: 'Tweet data' }, - replies: { type: 'any', description: 'Tweet replies' }, - context: { type: 'any', description: 'Tweet context' }, + replies: { type: 'json', description: 'Tweet replies' }, + context: { type: 'json', description: 'Tweet context' }, tweets: { type: 'json', description: 'Tweets data' }, - includes: { type: 'any', description: 'Additional data' }, + includes: { type: 'json', description: 'Additional data' }, meta: { type: 'json', description: 'Response metadata' }, user: { type: 'json', description: 'User profile data' }, - recentTweets: { type: 'any', description: 'Recent tweets data' }, + recentTweets: { type: 'json', description: 'Recent tweets data' }, }, } diff --git a/apps/sim/blocks/registry.ts b/apps/sim/blocks/registry.ts index 9cde501eb0..b052b80ae4 100644 --- a/apps/sim/blocks/registry.ts +++ b/apps/sim/blocks/registry.ts @@ -18,6 +18,7 @@ import { ExaBlock } from '@/blocks/blocks/exa' import { FileBlock } from '@/blocks/blocks/file' import { FirecrawlBlock } from '@/blocks/blocks/firecrawl' import { FunctionBlock } from '@/blocks/blocks/function' +import { GenericWebhookBlock } from '@/blocks/blocks/generic_webhook' import { GitHubBlock } from '@/blocks/blocks/github' import { GmailBlock } from '@/blocks/blocks/gmail' import { GoogleSearchBlock } from '@/blocks/blocks/google' @@ -91,6 +92,7 @@ export const registry: Record = { firecrawl: FirecrawlBlock, file: FileBlock, function: FunctionBlock, + generic_webhook: GenericWebhookBlock, github: GitHubBlock, gmail: GmailBlock, google_calendar: GoogleCalendarBlock, diff --git a/apps/sim/blocks/types.ts b/apps/sim/blocks/types.ts index 8cd1391742..d1a6d7ca18 100644 --- a/apps/sim/blocks/types.ts +++ b/apps/sim/blocks/types.ts @@ -36,6 +36,7 @@ export type SubBlockType = | 'time-input' // Time input | 'oauth-input' // OAuth credential selector | 'webhook-config' // Webhook configuration + | 'trigger-config' // Trigger configuration | 'schedule-config' // Schedule status and information | 'file-selector' // File selector for Google Drive, etc. | 'project-selector' // Project selector for Jira, Discord, etc. @@ -167,6 +168,9 @@ export interface SubBlockConfig { placeholder?: string // Custom placeholder for the prompt input maintainHistory?: boolean // Whether to maintain conversation history } + // Trigger-specific configuration + availableTriggers?: string[] // List of trigger IDs available for this subblock + triggerProvider?: string // Which provider's triggers to show } // Main block definition @@ -195,6 +199,10 @@ export interface BlockConfig { } } hideFromToolbar?: boolean + triggers?: { + enabled: boolean + available: string[] // List of trigger IDs this block supports + } } // Output configuration rules diff --git a/apps/sim/components/ui/tag-dropdown.test.tsx b/apps/sim/components/ui/tag-dropdown.test.tsx index 2889f7e139..c2bf947017 100644 --- a/apps/sim/components/ui/tag-dropdown.test.tsx +++ b/apps/sim/components/ui/tag-dropdown.test.tsx @@ -4,6 +4,115 @@ import { extractFieldsFromSchema, parseResponseFormatSafely } from '@/lib/respon import type { BlockState } from '@/stores/workflows/workflow/types' import { generateLoopBlocks } from '@/stores/workflows/workflow/utils' +// Mock getTool function for testing tool output types +vi.mock('@/lib/get-tool', () => ({ + getTool: vi.fn((toolId: string) => { + // Mock different tool configurations for testing + const mockTools: Record = { + exa_search: { + outputs: { + results: { + type: 'array', + description: 'Search results with titles, URLs, and text snippets', + items: { + type: 'object', + properties: { + title: { type: 'string', description: 'The title of the search result' }, + url: { type: 'string', description: 'The URL of the search result' }, + score: { type: 'number', description: 'Relevance score for the search result' }, + }, + }, + }, + }, + }, + pinecone_search_text: { + outputs: { + matches: { + type: 'array', + description: 'Search results with ID, score, and metadata', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Vector ID' }, + score: { type: 'number', description: 'Similarity score' }, + metadata: { type: 'object', description: 'Associated metadata' }, + }, + }, + }, + usage: { + type: 'object', + description: 'Usage statistics including tokens, read units, and rerank units', + properties: { + total_tokens: { type: 'number', description: 'Total tokens used for embedding' }, + read_units: { type: 'number', description: 'Read units consumed' }, + rerank_units: { type: 'number', description: 'Rerank units used' }, + }, + }, + }, + }, + notion_query_database: { + outputs: { + content: { + type: 'string', + description: 'Formatted list of database entries with their properties', + }, + metadata: { + type: 'object', + description: + 'Query metadata including total results count, pagination info, and raw results array', + properties: { + totalResults: { type: 'number', description: 'Number of results returned' }, + hasMore: { type: 'boolean', description: 'Whether more results are available' }, + results: { + type: 'array', + description: 'Raw Notion page objects', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Page ID' }, + properties: { type: 'object', description: 'Page properties' }, + }, + }, + }, + }, + }, + }, + }, + } + return mockTools[toolId] || null + }), +})) + +// Mock getBlock function for testing +vi.mock('@/lib/get-block', () => ({ + getBlock: vi.fn((blockType: string) => { + const mockBlockConfigs: Record = { + exa: { + tools: { + config: { + tool: ({ operation }: { operation: string }) => `exa_${operation}`, + }, + }, + }, + tools: { + tools: { + config: { + tool: ({ operation }: { operation: string }) => `pinecone_${operation}`, + }, + }, + }, + notion: { + tools: { + config: { + tool: ({ operation }: { operation: string }) => `notion_${operation}`, + }, + }, + }, + } + return mockBlockConfigs[blockType] || null + }), +})) + vi.mock('@/stores/workflows/workflow/store', () => ({ useWorkflowStore: vi.fn(() => ({ blocks: {}, @@ -34,6 +143,633 @@ vi.mock('@/stores/workflows/subblock/store', () => ({ })), })) +// Mock trigger functions +vi.mock('@/triggers/utils', () => ({ + getTriggersByProvider: vi.fn((provider: string) => { + const mockTriggers: Record = { + outlook: [ + { + id: 'outlook_poller', + name: 'Outlook Email Trigger', + outputs: { + email: { + id: { type: 'string', description: 'Outlook message ID' }, + conversationId: { type: 'string', description: 'Outlook conversation ID' }, + subject: { type: 'string', description: 'Email subject line' }, + hasAttachments: { type: 'boolean', description: 'Whether email has attachments' }, + isRead: { type: 'boolean', description: 'Whether email is read' }, + from: { type: 'string', description: 'Email sender' }, + to: { type: 'string', description: 'Email recipient' }, + cc: { type: 'string', description: 'CC recipients' }, + date: { type: 'string', description: 'Email date' }, + bodyText: { type: 'string', description: 'Email body text' }, + bodyHtml: { type: 'string', description: 'Email body HTML' }, + folderId: { type: 'string', description: 'Folder ID' }, + messageId: { type: 'string', description: 'Message ID' }, + threadId: { type: 'string', description: 'Thread ID' }, + }, + timestamp: { type: 'string', description: 'Event timestamp' }, + rawEmail: { + type: 'json', + description: 'Complete raw email data from Microsoft Graph API', + }, + }, + }, + ], + slack: [ + { + id: 'slack_message', + name: 'Slack Message Trigger', + outputs: { + message: { + text: { type: 'string', description: 'Message text' }, + user: { type: 'string', description: 'User ID' }, + channel: { type: 'string', description: 'Channel ID' }, + timestamp: { type: 'string', description: 'Message timestamp' }, + }, + channel: { type: 'string', description: 'Channel information' }, + }, + }, + ], + } + return mockTriggers[provider] || [] + }), +})) + +describe('TagDropdown Trigger Output Parsing', () => { + it.concurrent('should parse trigger outputs correctly for outlook trigger', () => { + // Mock getTriggersByProvider function directly + const getTriggersByProvider = vi.fn((provider: string) => { + const mockTriggers: Record = { + outlook: [ + { + id: 'outlook_poller', + name: 'Outlook Email Trigger', + outputs: { + email: { + id: { type: 'string', description: 'Outlook message ID' }, + conversationId: { type: 'string', description: 'Outlook conversation ID' }, + subject: { type: 'string', description: 'Email subject line' }, + hasAttachments: { type: 'boolean', description: 'Whether email has attachments' }, + isRead: { type: 'boolean', description: 'Whether email is read' }, + }, + timestamp: { type: 'string', description: 'Event timestamp' }, + rawEmail: { type: 'json', description: 'Complete raw email data' }, + }, + }, + ], + } + return mockTriggers[provider] || [] + }) + + const triggers = getTriggersByProvider('outlook') + const firstTrigger = triggers[0] + + expect(firstTrigger).toBeDefined() + expect(firstTrigger.outputs).toBeDefined() + expect(firstTrigger.outputs.email).toBeDefined() + expect(firstTrigger.outputs.timestamp).toBeDefined() + expect(firstTrigger.outputs.rawEmail).toBeDefined() + + // Verify email nested properties + expect(firstTrigger.outputs.email.id.type).toBe('string') + expect(firstTrigger.outputs.email.subject.type).toBe('string') + expect(firstTrigger.outputs.email.hasAttachments.type).toBe('boolean') + expect(firstTrigger.outputs.email.isRead.type).toBe('boolean') + }) + + it.concurrent( + 'should get correct output type for trigger paths using getOutputTypeForPath', + () => { + // Mock getTriggersByProvider function directly + const getTriggersByProvider = vi.fn((provider: string) => { + const mockTriggers: Record = { + outlook: [ + { + id: 'outlook_poller', + outputs: { + email: { + id: { type: 'string' }, + subject: { type: 'string' }, + hasAttachments: { type: 'boolean' }, + isRead: { type: 'boolean' }, + from: { type: 'string' }, + to: { type: 'string' }, + }, + timestamp: { type: 'string' }, + rawEmail: { type: 'json' }, + }, + }, + ], + } + return mockTriggers[provider] || [] + }) + + // Mock the getOutputTypeForPath function behavior for triggers + const getOutputTypeForPath = ( + block: any, + blockConfig: any, + blockId: string, + outputPath: string + ): string => { + if (block?.triggerMode && blockConfig?.triggers?.enabled) { + const triggers = getTriggersByProvider(block.type) + const firstTrigger = triggers[0] + + if (firstTrigger?.outputs) { + const pathParts = outputPath.split('.') + let currentObj: any = firstTrigger.outputs + + for (const part of pathParts) { + if (currentObj && typeof currentObj === 'object') { + currentObj = currentObj[part] + } else { + break + } + } + + if ( + currentObj && + typeof currentObj === 'object' && + 'type' in currentObj && + currentObj.type + ) { + return currentObj.type + } + } + } + + return 'any' + } + + const block = { + id: 'outlook1', + type: 'outlook', + triggerMode: true, + } + + const blockConfig = { + triggers: { enabled: true }, + } + + // Test top-level trigger outputs + expect(getOutputTypeForPath(block, blockConfig, 'outlook1', 'timestamp')).toBe('string') + expect(getOutputTypeForPath(block, blockConfig, 'outlook1', 'rawEmail')).toBe('json') + + // Test nested email properties + expect(getOutputTypeForPath(block, blockConfig, 'outlook1', 'email.id')).toBe('string') + expect(getOutputTypeForPath(block, blockConfig, 'outlook1', 'email.subject')).toBe('string') + expect(getOutputTypeForPath(block, blockConfig, 'outlook1', 'email.hasAttachments')).toBe( + 'boolean' + ) + expect(getOutputTypeForPath(block, blockConfig, 'outlook1', 'email.isRead')).toBe('boolean') + expect(getOutputTypeForPath(block, blockConfig, 'outlook1', 'email.from')).toBe('string') + expect(getOutputTypeForPath(block, blockConfig, 'outlook1', 'email.to')).toBe('string') + + // Test non-existent paths + expect(getOutputTypeForPath(block, blockConfig, 'outlook1', 'email.nonexistent')).toBe('any') + expect(getOutputTypeForPath(block, blockConfig, 'outlook1', 'nonexistent')).toBe('any') + } + ) + + it.concurrent('should handle trigger output navigation for parent objects', () => { + const getTriggersByProvider = vi.fn((provider: string) => { + const mockTriggers: Record = { + outlook: [ + { + outputs: { + email: { + id: { type: 'string' }, + subject: { type: 'string' }, + }, + timestamp: { type: 'string' }, + }, + }, + ], + } + return mockTriggers[provider] || [] + }) + + const getOutputTypeForPath = ( + block: any, + blockConfig: any, + blockId: string, + outputPath: string + ): string => { + if (block?.triggerMode && blockConfig?.triggers?.enabled) { + const triggers = getTriggersByProvider(block.type) + const firstTrigger = triggers[0] + + if (firstTrigger?.outputs) { + const pathParts = outputPath.split('.') + let currentObj: any = firstTrigger.outputs + + for (const part of pathParts) { + if (currentObj && typeof currentObj === 'object') { + currentObj = currentObj[part] + } else { + break + } + } + + if ( + currentObj && + typeof currentObj === 'object' && + 'type' in currentObj && + currentObj.type + ) { + return currentObj.type + } + + // Check if currentObj is a parent object with nested properties + if (currentObj && typeof currentObj === 'object' && !('type' in currentObj)) { + return 'object' + } + } + } + + return 'any' + } + + const block = { + id: 'outlook1', + type: 'outlook', + triggerMode: true, + } + + const blockConfig = { + triggers: { enabled: true }, + } + + // Test parent object (email should be treated as object type) + expect(getOutputTypeForPath(block, blockConfig, 'outlook1', 'email')).toBe('object') + }) + + it.concurrent('should return "any" for non-trigger blocks', () => { + const getOutputTypeForPath = ( + block: any, + blockConfig: any, + blockId: string, + outputPath: string + ): string => { + if (block?.triggerMode && blockConfig?.triggers?.enabled) { + const { getTriggersByProvider } = require('@/triggers/utils') + const triggers = getTriggersByProvider(block.type) + const firstTrigger = triggers[0] + + if (firstTrigger?.outputs) { + const pathParts = outputPath.split('.') + let currentObj: any = firstTrigger.outputs + + for (const part of pathParts) { + if (currentObj && typeof currentObj === 'object') { + currentObj = currentObj[part] + } else { + break + } + } + + if ( + currentObj && + typeof currentObj === 'object' && + 'type' in currentObj && + currentObj.type + ) { + return currentObj.type + } + } + } + + return 'any' + } + + // Test block without trigger mode + const normalBlock = { + id: 'outlook1', + type: 'outlook', + triggerMode: false, + } + + const blockConfig = { + triggers: { enabled: true }, + } + + expect(getOutputTypeForPath(normalBlock, blockConfig, 'outlook1', 'email.id')).toBe('any') + + // Test block with trigger mode but triggers not enabled + const triggerBlockNoConfig = { + id: 'outlook1', + type: 'outlook', + triggerMode: true, + } + + const noTriggersConfig = { + triggers: { enabled: false }, + } + + expect( + getOutputTypeForPath(triggerBlockNoConfig, noTriggersConfig, 'outlook1', 'email.id') + ).toBe('any') + }) + + it.concurrent('should handle different trigger providers correctly', () => { + const getTriggersByProvider = vi.fn((provider: string) => { + const mockTriggers: Record = { + slack: [ + { + outputs: { + message: { + text: { type: 'string' }, + user: { type: 'string' }, + channel: { type: 'string' }, + }, + channel: { type: 'string' }, + }, + }, + ], + } + return mockTriggers[provider] || [] + }) + + const getOutputTypeForPath = ( + block: any, + blockConfig: any, + blockId: string, + outputPath: string + ): string => { + if (block?.triggerMode && blockConfig?.triggers?.enabled) { + const triggers = getTriggersByProvider(block.type) + const firstTrigger = triggers[0] + + if (firstTrigger?.outputs) { + const pathParts = outputPath.split('.') + let currentObj: any = firstTrigger.outputs + + for (const part of pathParts) { + if (currentObj && typeof currentObj === 'object') { + currentObj = currentObj[part] + } else { + break + } + } + + if ( + currentObj && + typeof currentObj === 'object' && + 'type' in currentObj && + currentObj.type + ) { + return currentObj.type + } + } + } + + return 'any' + } + + // Test Slack trigger + const slackBlock = { + id: 'slack1', + type: 'slack', + triggerMode: true, + } + + const blockConfig = { + triggers: { enabled: true }, + } + + expect(getOutputTypeForPath(slackBlock, blockConfig, 'slack1', 'message.text')).toBe('string') + expect(getOutputTypeForPath(slackBlock, blockConfig, 'slack1', 'message.user')).toBe('string') + expect(getOutputTypeForPath(slackBlock, blockConfig, 'slack1', 'channel')).toBe('string') + }) + + it.concurrent('should handle malformed or missing trigger configurations gracefully', () => { + const getOutputTypeForPath = ( + block: any, + blockConfig: any, + blockId: string, + outputPath: string + ): string => { + if (block?.triggerMode && blockConfig?.triggers?.enabled) { + try { + const { getTriggersByProvider } = require('@/triggers/utils') + const triggers = getTriggersByProvider(block.type) + const firstTrigger = triggers[0] + + if (firstTrigger?.outputs) { + const pathParts = outputPath.split('.') + let currentObj: any = firstTrigger.outputs + + for (const part of pathParts) { + if (currentObj && typeof currentObj === 'object') { + currentObj = currentObj[part] + } else { + break + } + } + + if ( + currentObj && + typeof currentObj === 'object' && + 'type' in currentObj && + currentObj.type + ) { + return currentObj.type + } + } + } catch (error) { + return 'any' + } + } + + return 'any' + } + + // Test with unknown trigger provider + const unknownBlock = { + id: 'unknown1', + type: 'unknown_provider', + triggerMode: true, + } + + const blockConfig = { + triggers: { enabled: true }, + } + + expect(getOutputTypeForPath(unknownBlock, blockConfig, 'unknown1', 'any.path')).toBe('any') + + // Test with null/undefined configurations + expect(getOutputTypeForPath(null, blockConfig, 'test', 'path')).toBe('any') + expect(getOutputTypeForPath(unknownBlock, null, 'test', 'path')).toBe('any') + }) + + it.concurrent('should generate correct trigger output tags for dropdown', () => { + const getTriggersByProvider = vi.fn((provider: string) => { + const mockTriggers: Record = { + outlook: [ + { + outputs: { + email: { + id: { type: 'string' }, + subject: { type: 'string' }, + hasAttachments: { type: 'boolean' }, + isRead: { type: 'boolean' }, + }, + timestamp: { type: 'string' }, + rawEmail: { type: 'json' }, + }, + }, + ], + slack: [ + { + outputs: { + message: { + text: { type: 'string' }, + user: { type: 'string' }, + }, + channel: { type: 'string' }, + }, + }, + ], + } + return mockTriggers[provider] || [] + }) + + // Mock trigger output tag generation + const generateTriggerOutputTags = (blockType: string, blockId: string): string[] => { + const triggers = getTriggersByProvider(blockType) + const firstTrigger = triggers[0] + + if (!firstTrigger?.outputs) return [] + + const tags: string[] = [] + const normalizedBlockId = blockId.replace(/\s+/g, '').toLowerCase() + + const traverseOutputs = (outputs: any, prefix = '') => { + for (const [key, output] of Object.entries(outputs)) { + const currentPath = prefix ? `${prefix}.${key}` : key + const fullTag = `${normalizedBlockId}.${currentPath}` + + tags.push(fullTag) + + // If this is a parent object with nested properties, recurse + if (output && typeof output === 'object' && !('type' in output)) { + traverseOutputs(output, currentPath) + } + } + } + + traverseOutputs(firstTrigger.outputs) + return tags + } + + // Test Outlook trigger tags + const outlookTags = generateTriggerOutputTags('outlook', 'Outlook 1') + + expect(outlookTags).toContain('outlook1.email') + expect(outlookTags).toContain('outlook1.email.id') + expect(outlookTags).toContain('outlook1.email.subject') + expect(outlookTags).toContain('outlook1.email.hasAttachments') + expect(outlookTags).toContain('outlook1.email.isRead') + expect(outlookTags).toContain('outlook1.timestamp') + expect(outlookTags).toContain('outlook1.rawEmail') + + // Test Slack trigger tags + const slackTags = generateTriggerOutputTags('slack', 'Slack 1') + + expect(slackTags).toContain('slack1.message') + expect(slackTags).toContain('slack1.message.text') + expect(slackTags).toContain('slack1.message.user') + expect(slackTags).toContain('slack1.channel') + }) + + it.concurrent('should correctly identify trigger vs tool output resolution', () => { + const getTriggersByProvider = vi.fn((provider: string) => { + const mockTriggers: Record = { + outlook: [ + { + outputs: { + email: { + id: { type: 'string' }, + }, + }, + }, + ], + } + return mockTriggers[provider] || [] + }) + + const getOutputTypeForPath = ( + block: any, + blockConfig: any, + blockId: string, + outputPath: string + ): string => { + if (block?.triggerMode && blockConfig?.triggers?.enabled) { + // Trigger mode logic + const triggers = getTriggersByProvider(block.type) + const firstTrigger = triggers[0] + + if (firstTrigger?.outputs) { + const pathParts = outputPath.split('.') + let currentObj: any = firstTrigger.outputs + + for (const part of pathParts) { + if (currentObj && typeof currentObj === 'object') { + currentObj = currentObj[part] + } else { + break + } + } + + if ( + currentObj && + typeof currentObj === 'object' && + 'type' in currentObj && + currentObj.type + ) { + return currentObj.type + } + } + } else { + // Tool mode logic - simplified mock + if (blockConfig && outputPath === 'results') { + return 'array' + } + } + + return 'any' + } + + // Test trigger mode + const triggerBlock = { + id: 'outlook1', + type: 'outlook', + triggerMode: true, + } + + const triggerConfig = { + triggers: { enabled: true }, + } + + expect(getOutputTypeForPath(triggerBlock, triggerConfig, 'outlook1', 'email.id')).toBe('string') + + // Test tool mode + const toolBlock = { + id: 'outlook1', + type: 'outlook', + triggerMode: false, + } + + const toolConfig = { + triggers: { enabled: false }, + } + + expect(getOutputTypeForPath(toolBlock, toolConfig, 'outlook1', 'results')).toBe('array') + expect(getOutputTypeForPath(toolBlock, toolConfig, 'outlook1', 'email.id')).toBe('any') + }) +}) + describe('TagDropdown Loop Suggestions', () => { it.concurrent('should generate correct loop suggestions for forEach loops', () => { const blocks: Record = { @@ -790,3 +1526,466 @@ describe('TagDropdown Response Format Support', () => { ]) }) }) + +describe('TagDropdown Type Display Functionality', () => { + it.concurrent( + 'should extract types correctly from tool outputs using generateOutputPathsWithTypes', + () => { + // Test with Exa search tool outputs + const exaSearchOutputs = { + results: { + type: 'array', + description: 'Search results with titles, URLs, and text snippets', + items: { + type: 'object', + properties: { + title: { type: 'string', description: 'The title of the search result' }, + url: { type: 'string', description: 'The URL of the search result' }, + score: { type: 'number', description: 'Relevance score for the search result' }, + }, + }, + }, + } + + // Mock the generateOutputPathsWithTypes function behavior + const generateOutputPathsWithTypes = ( + outputs: Record, + prefix = '' + ): Array<{ path: string; type: string }> => { + const paths: Array<{ path: string; type: string }> = [] + + for (const [key, output] of Object.entries(outputs)) { + const currentPath = prefix ? `${prefix}.${key}` : key + if (output && typeof output === 'object' && 'type' in output) { + paths.push({ path: currentPath, type: output.type as string }) + + // Handle nested properties + if ((output as any).properties) { + const nestedPaths = generateOutputPathsWithTypes( + (output as any).properties, + currentPath + ) + paths.push(...nestedPaths) + } + + // Handle array items properties + if ((output as any).items?.properties) { + const itemPaths = generateOutputPathsWithTypes( + (output as any).items.properties, + currentPath + ) + paths.push(...itemPaths) + } + } + } + + return paths + } + + const paths = generateOutputPathsWithTypes(exaSearchOutputs) + + expect(paths).toEqual([ + { path: 'results', type: 'array' }, + { path: 'results.title', type: 'string' }, + { path: 'results.url', type: 'string' }, + { path: 'results.score', type: 'number' }, + ]) + } + ) + + it.concurrent('should extract types correctly for complex nested structures', () => { + // Test with Pinecone tool outputs + const pineconeOutputs = { + matches: { + type: 'array', + description: 'Search results with ID, score, and metadata', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Vector ID' }, + score: { type: 'number', description: 'Similarity score' }, + metadata: { type: 'object', description: 'Associated metadata' }, + }, + }, + }, + usage: { + type: 'object', + description: 'Usage statistics including tokens, read units, and rerank units', + properties: { + total_tokens: { type: 'number', description: 'Total tokens used for embedding' }, + read_units: { type: 'number', description: 'Read units consumed' }, + rerank_units: { type: 'number', description: 'Rerank units used' }, + }, + }, + } + + const generateOutputPathsWithTypes = ( + outputs: Record, + prefix = '' + ): Array<{ path: string; type: string }> => { + const paths: Array<{ path: string; type: string }> = [] + + for (const [key, output] of Object.entries(outputs)) { + const currentPath = prefix ? `${prefix}.${key}` : key + if (output && typeof output === 'object' && 'type' in output) { + paths.push({ path: currentPath, type: output.type as string }) + + if ((output as any).properties) { + const nestedPaths = generateOutputPathsWithTypes( + (output as any).properties, + currentPath + ) + paths.push(...nestedPaths) + } + + if ((output as any).items?.properties) { + const itemPaths = generateOutputPathsWithTypes( + (output as any).items.properties, + currentPath + ) + paths.push(...itemPaths) + } + } + } + + return paths + } + + const paths = generateOutputPathsWithTypes(pineconeOutputs) + + expect(paths).toEqual([ + { path: 'matches', type: 'array' }, + { path: 'matches.id', type: 'string' }, + { path: 'matches.score', type: 'number' }, + { path: 'matches.metadata', type: 'object' }, + { path: 'usage', type: 'object' }, + { path: 'usage.total_tokens', type: 'number' }, + { path: 'usage.read_units', type: 'number' }, + { path: 'usage.rerank_units', type: 'number' }, + ]) + }) + + it.concurrent('should get tool output type for specific paths using getToolOutputType', () => { + // Mock block configuration for Exa + const blockConfig = { + tools: { + config: { + tool: ({ operation }: { operation: string }) => `exa_${operation}`, + }, + }, + } + + // Mock getToolOutputType function behavior + const getToolOutputType = (blockConfig: any, operation: string, path: string): string => { + // Get tool ID from block config + const toolId = blockConfig?.tools?.config?.tool?.({ operation }) + if (!toolId) return '' + + // Mock tool lookup (would use getTool in real implementation) + const mockTools: Record = { + exa_search: { + outputs: { + results: { + type: 'array', + items: { + type: 'object', + properties: { + title: { type: 'string' }, + url: { type: 'string' }, + score: { type: 'number' }, + }, + }, + }, + }, + }, + } + + const tool = mockTools[toolId] + if (!tool?.outputs) return '' + + // Navigate to the specific path + const pathParts = path.split('.') + let current = tool.outputs + + for (const part of pathParts) { + if (!current[part]) { + // Check if we're looking at array items + if (current.items?.properties?.[part]) { + current = current.items.properties + } else { + return '' + } + } + current = current[part] + } + + return current?.type || '' + } + + // Test various path types + expect(getToolOutputType(blockConfig, 'search', 'results')).toBe('array') + expect(getToolOutputType(blockConfig, 'search', 'results.title')).toBe('string') + expect(getToolOutputType(blockConfig, 'search', 'results.url')).toBe('string') + expect(getToolOutputType(blockConfig, 'search', 'results.score')).toBe('number') + expect(getToolOutputType(blockConfig, 'search', 'nonexistent')).toBe('') + }) + + it.concurrent('should generate tool output paths with type information', () => { + // Mock the generateToolOutputPaths function that returns both path and type + const generateToolOutputPaths = ( + blockConfig: any, + operation: string + ): Array<{ path: string; type: string }> => { + const toolId = blockConfig?.tools?.config?.tool?.({ operation }) + if (!toolId) return [] + + // Mock tool configurations + const mockTools: Record = { + exa_search: { + outputs: { + results: { + type: 'array', + items: { + type: 'object', + properties: { + title: { type: 'string' }, + url: { type: 'string' }, + score: { type: 'number' }, + }, + }, + }, + }, + }, + } + + const tool = mockTools[toolId] + if (!tool?.outputs) return [] + + const paths: Array<{ path: string; type: string }> = [] + + const traverse = (obj: any, prefix = '') => { + for (const [key, value] of Object.entries(obj)) { + const currentPath = prefix ? `${prefix}.${key}` : key + if (value && typeof value === 'object' && 'type' in value) { + paths.push({ path: currentPath, type: (value as any).type }) + + if ((value as any).properties) { + traverse((value as any).properties, currentPath) + } + + if ((value as any).items?.properties) { + traverse((value as any).items.properties, currentPath) + } + } + } + } + + traverse(tool.outputs) + return paths + } + + const blockConfig = { + tools: { + config: { + tool: ({ operation }: { operation: string }) => `exa_${operation}`, + }, + }, + } + + const paths = generateToolOutputPaths(blockConfig, 'search') + + expect(paths).toEqual([ + { path: 'results', type: 'array' }, + { path: 'results.title', type: 'string' }, + { path: 'results.url', type: 'string' }, + { path: 'results.score', type: 'number' }, + ]) + }) + + it.concurrent('should handle missing or invalid tool configurations gracefully', () => { + const getToolOutputType = (blockConfig: any, operation: string, path: string): string => { + try { + const toolId = blockConfig?.tools?.config?.tool?.({ operation }) + if (!toolId) return '' + + // Mock empty tool configurations + const mockTools: Record = {} + const tool = mockTools[toolId] + if (!tool?.outputs) return '' + + return '' + } catch (error) { + return '' + } + } + + // Test with null/undefined block config + expect(getToolOutputType(null, 'search', 'results')).toBe('') + expect(getToolOutputType(undefined, 'search', 'results')).toBe('') + expect(getToolOutputType({}, 'search', 'results')).toBe('') + + // Test with invalid block config structure + const invalidBlockConfig = { tools: null } + expect(getToolOutputType(invalidBlockConfig, 'search', 'results')).toBe('') + + // Test with missing tool function + const incompleteBlockConfig = { + tools: { + config: {}, + }, + } + expect(getToolOutputType(incompleteBlockConfig, 'search', 'results')).toBe('') + }) + + it.concurrent( + 'should only show types when reliable data is available from tool configuration', + () => { + // Mock tag info creation that only includes type when available + const createTagInfo = ( + blockConfig: any, + operation: string, + path: string + ): { type?: string; description?: string } => { + const getToolOutputType = (blockConfig: any, operation: string, path: string): string => { + const toolId = blockConfig?.tools?.config?.tool?.({ operation }) + if (!toolId) return '' + + const mockTools: Record = { + exa_search: { + outputs: { + results: { + type: 'array', + items: { + type: 'object', + properties: { + title: { type: 'string' }, + }, + }, + }, + }, + }, + } + + const tool = mockTools[toolId] + if (!tool?.outputs) return '' + + const pathParts = path.split('.') + let current = tool.outputs + + for (const part of pathParts) { + if (!current[part]) { + if ((current as any).items?.properties?.[part]) { + current = (current as any).items.properties + } else { + return '' + } + } + current = current[part] + } + + return (current as any)?.type || '' + } + + const type = getToolOutputType(blockConfig, operation, path) + + // Only return type information if we have reliable data + if (type) { + return { type } + } + + return {} + } + + const blockConfig = { + tools: { + config: { + tool: ({ operation }: { operation: string }) => `exa_${operation}`, + }, + }, + } + + // Should have type for valid paths + expect(createTagInfo(blockConfig, 'search', 'results')).toEqual({ type: 'array' }) + expect(createTagInfo(blockConfig, 'search', 'results.title')).toEqual({ type: 'string' }) + + // Should not have type for invalid paths + expect(createTagInfo(blockConfig, 'search', 'nonexistent')).toEqual({}) + expect(createTagInfo(blockConfig, 'invalid_operation', 'results')).toEqual({}) + expect(createTagInfo(null, 'search', 'results')).toEqual({}) + } + ) + + it.concurrent('should handle deeply nested structures correctly', () => { + // Test with Notion query_database tool structure + const notionOutputs = { + content: { + type: 'string', + description: 'Formatted list of database entries with their properties', + }, + metadata: { + type: 'object', + description: + 'Query metadata including total results count, pagination info, and raw results array', + properties: { + totalResults: { type: 'number', description: 'Number of results returned' }, + hasMore: { type: 'boolean', description: 'Whether more results are available' }, + results: { + type: 'array', + description: 'Raw Notion page objects', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Page ID' }, + properties: { type: 'object', description: 'Page properties' }, + }, + }, + }, + }, + }, + } + + const generateOutputPathsWithTypes = ( + outputs: Record, + prefix = '' + ): Array<{ path: string; type: string }> => { + const paths: Array<{ path: string; type: string }> = [] + + for (const [key, output] of Object.entries(outputs)) { + const currentPath = prefix ? `${prefix}.${key}` : key + if (output && typeof output === 'object' && 'type' in output) { + paths.push({ path: currentPath, type: output.type as string }) + + if ((output as any).properties) { + const nestedPaths = generateOutputPathsWithTypes( + (output as any).properties, + currentPath + ) + paths.push(...nestedPaths) + } + + if ((output as any).items?.properties) { + const itemPaths = generateOutputPathsWithTypes( + (output as any).items.properties, + currentPath + ) + paths.push(...itemPaths) + } + } + } + + return paths + } + + const paths = generateOutputPathsWithTypes(notionOutputs) + + expect(paths).toEqual([ + { path: 'content', type: 'string' }, + { path: 'metadata', type: 'object' }, + { path: 'metadata.totalResults', type: 'number' }, + { path: 'metadata.hasMore', type: 'boolean' }, + { path: 'metadata.results', type: 'array' }, + { path: 'metadata.results.id', type: 'string' }, + { path: 'metadata.results.properties', type: 'object' }, + ]) + }) +}) diff --git a/apps/sim/components/ui/tag-dropdown.tsx b/apps/sim/components/ui/tag-dropdown.tsx index 12dbdd6a2e..272d59cdbb 100644 --- a/apps/sim/components/ui/tag-dropdown.tsx +++ b/apps/sim/components/ui/tag-dropdown.tsx @@ -1,5 +1,6 @@ import type React from 'react' import { useCallback, useEffect, useMemo, useState } from 'react' +import { ChevronRight } from 'lucide-react' import { BlockPathCalculator } from '@/lib/block-path-calculator' import { extractFieldsFromSchema, parseResponseFormatSafely } from '@/lib/response-format' import { cn } from '@/lib/utils' @@ -10,6 +11,8 @@ import type { Variable } from '@/stores/panel/variables/types' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' import { useSubBlockStore } from '@/stores/workflows/subblock/store' import { useWorkflowStore } from '@/stores/workflows/workflow/store' +import { getTool } from '@/tools/utils' +import { getTriggersByProvider } from '@/triggers' interface BlockTagGroup { blockName: string @@ -31,7 +34,6 @@ interface TagDropdownProps { style?: React.CSSProperties } -// Check if tag trigger '<' should show dropdown export const checkTagTrigger = (text: string, cursorPosition: number): { show: boolean } => { if (cursorPosition >= 1) { const textBeforeCursor = text.slice(0, cursorPosition) @@ -46,7 +48,110 @@ export const checkTagTrigger = (text: string, cursorPosition: number): { show: b return { show: false } } -// Generate output paths from block configuration outputs +const BLOCK_COLORS = { + VARIABLE: '#2F8BFF', + DEFAULT: '#2F55FF', + LOOP: '#8857E6', + PARALLEL: '#FF5757', +} as const + +const TAG_PREFIXES = { + VARIABLE: 'variable.', +} as const + +const normalizeBlockName = (blockName: string): string => { + return blockName.replace(/\s+/g, '').toLowerCase() +} + +const normalizeVariableName = (variableName: string): string => { + return variableName.replace(/\s+/g, '') +} + +const getSubBlockValue = (blockId: string, property: string): any => { + return useSubBlockStore.getState().getValue(blockId, property) +} + +const createTagEventHandlers = ( + tag: string, + group: any, + tagIndex: number, + handleTagSelect: (tag: string, group?: any) => void, + setSelectedIndex: (index: number) => void, + setHoveredNested: (value: any) => void +) => ({ + onMouseEnter: () => { + setSelectedIndex(tagIndex >= 0 ? tagIndex : 0) + setHoveredNested(null) + }, + onMouseDown: (e: React.MouseEvent) => { + e.preventDefault() + e.stopPropagation() + handleTagSelect(tag, group) + }, + onClick: (e: React.MouseEvent) => { + e.preventDefault() + e.stopPropagation() + handleTagSelect(tag, group) + }, +}) + +const getOutputTypeForPath = ( + block: any, + blockConfig: any, + blockId: string, + outputPath: string +): string => { + if (block?.triggerMode && blockConfig?.triggers?.enabled) { + const triggers = getTriggersByProvider(block.type) + const firstTrigger = triggers[0] + + if (firstTrigger?.outputs) { + const pathParts = outputPath.split('.') + let currentObj: any = firstTrigger.outputs + + for (const part of pathParts) { + if (currentObj && typeof currentObj === 'object') { + currentObj = currentObj[part] + } else { + break + } + } + + if (currentObj && typeof currentObj === 'object' && 'type' in currentObj && currentObj.type) { + return currentObj.type + } + } + } else if (block?.type === 'starter') { + // Handle starter block specific outputs + const startWorkflowValue = getSubBlockValue(blockId, 'startWorkflow') + + if (startWorkflowValue === 'chat') { + // Define types for chat mode outputs + const chatModeTypes: Record = { + input: 'string', + conversationId: 'string', + files: 'array', + } + return chatModeTypes[outputPath] || 'any' + } + // For API mode, check inputFormat for custom field types + const inputFormatValue = getSubBlockValue(blockId, 'inputFormat') + if (inputFormatValue && Array.isArray(inputFormatValue)) { + const field = inputFormatValue.find((f: any) => f.name === outputPath) + if (field?.type) { + return field.type + } + } + } else { + const operationValue = getSubBlockValue(blockId, 'operation') + if (blockConfig && operationValue) { + return getToolOutputType(blockConfig, operationValue, outputPath) + } + } + + return 'any' +} + const generateOutputPaths = (outputs: Record, prefix = ''): string[] => { const paths: string[] = [] @@ -62,7 +167,7 @@ const generateOutputPaths = (outputs: Record, prefix = ''): string[ // New format: { type: 'string', description: '...' } - treat as leaf node paths.push(currentPath) } else { - // Legacy nested object - recurse + // Nested object - recurse to get all child paths const subPaths = generateOutputPaths(value, currentPath) paths.push(...subPaths) } @@ -75,6 +180,94 @@ const generateOutputPaths = (outputs: Record, prefix = ''): string[ return paths } +const generateOutputPathsWithTypes = ( + outputs: Record, + prefix = '' +): Array<{ path: string; type: string }> => { + const paths: Array<{ path: string; type: string }> = [] + + for (const [key, value] of Object.entries(outputs)) { + const currentPath = prefix ? `${prefix}.${key}` : key + + if (typeof value === 'string') { + // Simple type like 'string', 'number', 'json', 'any' + paths.push({ path: currentPath, type: value }) + } else if (typeof value === 'object' && value !== null) { + // Check if this is our new format with type and description + if ('type' in value && typeof value.type === 'string') { + // Handle nested properties for arrays and objects + if (value.type === 'array' && value.items?.properties) { + // For arrays with properties, add the array itself and recurse into items + paths.push({ path: currentPath, type: 'array' }) + const subPaths = generateOutputPathsWithTypes(value.items.properties, currentPath) + paths.push(...subPaths) + } else if (value.type === 'object' && value.properties) { + // For objects with properties, add the object itself and recurse into properties + paths.push({ path: currentPath, type: 'object' }) + const subPaths = generateOutputPathsWithTypes(value.properties, currentPath) + paths.push(...subPaths) + } else { + // Leaf node - just add the type + paths.push({ path: currentPath, type: value.type }) + } + } else { + // Legacy nested object - recurse and assume 'object' type + const subPaths = generateOutputPathsWithTypes(value, currentPath) + paths.push(...subPaths) + } + } else { + // Fallback - add with 'any' type + paths.push({ path: currentPath, type: 'any' }) + } + } + + return paths +} + +const generateToolOutputPaths = (blockConfig: any, operation: string): string[] => { + if (!blockConfig?.tools?.config?.tool) return [] + + try { + // Get the tool ID for this operation + const toolId = blockConfig.tools.config.tool({ operation }) + if (!toolId) return [] + + // Get the tool configuration + const toolConfig = getTool(toolId) + if (!toolConfig?.outputs) return [] + + // Generate paths from tool outputs + return generateOutputPaths(toolConfig.outputs) + } catch (error) { + console.warn('Failed to get tool outputs for operation:', operation, error) + return [] + } +} + +const getToolOutputType = (blockConfig: any, operation: string, path: string): string => { + if (!blockConfig?.tools?.config?.tool) return 'any' + + try { + // Get the tool ID for this operation + const toolId = blockConfig.tools.config.tool({ operation }) + if (!toolId) return 'any' + + // Get the tool configuration + const toolConfig = getTool(toolId) + if (!toolConfig?.outputs) return 'any' + + // Generate paths with types from tool outputs + const pathsWithTypes = generateOutputPathsWithTypes(toolConfig.outputs) + + // Find the matching path and return its type + const matchingPath = pathsWithTypes.find((p) => p.path === path) + return matchingPath?.type || 'any' + } catch (error) { + console.warn('Failed to get tool output type for path:', path, error) + return 'any' + } +} + export const TagDropdown: React.FC = ({ visible, onSelect, @@ -86,43 +279,41 @@ export const TagDropdown: React.FC = ({ onClose, style, }) => { - // Component state const [selectedIndex, setSelectedIndex] = useState(0) + const [hoveredNested, setHoveredNested] = useState<{ tag: string; index: number } | null>(null) + const [inSubmenu, setInSubmenu] = useState(false) + const [submenuIndex, setSubmenuIndex] = useState(0) + const [parentHovered, setParentHovered] = useState(null) + const [submenuHovered, setSubmenuHovered] = useState(false) - // Store hooks for workflow data const blocks = useWorkflowStore((state) => state.blocks) const loops = useWorkflowStore((state) => state.loops) const parallels = useWorkflowStore((state) => state.parallels) const edges = useWorkflowStore((state) => state.edges) const workflowId = useWorkflowRegistry((state) => state.activeWorkflowId) - // Store hooks for variables const getVariablesByWorkflowId = useVariablesStore((state) => state.getVariablesByWorkflowId) const loadVariables = useVariablesStore((state) => state.loadVariables) const variables = useVariablesStore((state) => state.variables) const workflowVariables = workflowId ? getVariablesByWorkflowId(workflowId) : [] - // Load variables when workflow changes useEffect(() => { if (workflowId) { loadVariables(workflowId) } }, [workflowId, loadVariables]) - // Extract current search term from input const searchTerm = useMemo(() => { const textBeforeCursor = inputValue.slice(0, cursorPosition) const match = textBeforeCursor.match(/<([^>]*)$/) return match ? match[1].toLowerCase() : '' }, [inputValue, cursorPosition]) - // Generate all available tags using BlockPathCalculator and clean block outputs const { tags, variableInfoMap = {}, blockTagGroups = [], } = useMemo(() => { - // Handle active source block (drag & drop from specific block) if (activeSourceBlockId) { const sourceBlock = blocks[activeSourceBlockId] if (!sourceBlock) { @@ -131,19 +322,16 @@ export const TagDropdown: React.FC = ({ const blockConfig = getBlock(sourceBlock.type) - // Handle special blocks that aren't in the registry (loop and parallel) if (!blockConfig) { if (sourceBlock.type === 'loop' || sourceBlock.type === 'parallel') { - // Create a mock config with results output for loop/parallel blocks const mockConfig = { outputs: { - results: 'array', // These blocks have a results array output + results: 'array', }, } const blockName = sourceBlock.name || sourceBlock.type - const normalizedBlockName = blockName.replace(/\s+/g, '').toLowerCase() + const normalizedBlockName = normalizeBlockName(blockName) - // Generate output paths for the mock config const outputPaths = generateOutputPaths(mockConfig.outputs) const blockTags = outputPaths.map((path) => `${normalizedBlockName}.${path}`) @@ -167,81 +355,89 @@ export const TagDropdown: React.FC = ({ } const blockName = sourceBlock.name || sourceBlock.type - const normalizedBlockName = blockName.replace(/\s+/g, '').toLowerCase() + const normalizedBlockName = normalizeBlockName(blockName) - // Check for custom response format first - const responseFormatValue = useSubBlockStore - .getState() - .getValue(activeSourceBlockId, 'responseFormat') + const responseFormatValue = getSubBlockValue(activeSourceBlockId, 'responseFormat') const responseFormat = parseResponseFormatSafely(responseFormatValue, activeSourceBlockId) let blockTags: string[] - // Special handling for evaluator blocks if (sourceBlock.type === 'evaluator') { - // Get the evaluation metrics for the evaluator block - const metricsValue = useSubBlockStore.getState().getValue(activeSourceBlockId, 'metrics') + const metricsValue = getSubBlockValue(activeSourceBlockId, 'metrics') if (metricsValue && Array.isArray(metricsValue) && metricsValue.length > 0) { - // Use the metric names as the available outputs const validMetrics = metricsValue.filter((metric: any) => metric?.name) blockTags = validMetrics.map( (metric: any) => `${normalizedBlockName}.${metric.name.toLowerCase()}` ) } else { - // Fallback to default evaluator outputs if no metrics are defined const outputPaths = generateOutputPaths(blockConfig.outputs) blockTags = outputPaths.map((path) => `${normalizedBlockName}.${path}`) } } else if (responseFormat) { - // Use custom schema properties if response format is specified const schemaFields = extractFieldsFromSchema(responseFormat) if (schemaFields.length > 0) { blockTags = schemaFields.map((field) => `${normalizedBlockName}.${field.name}`) } else { - // Fallback to default if schema extraction failed const outputPaths = generateOutputPaths(blockConfig.outputs || {}) blockTags = outputPaths.map((path) => `${normalizedBlockName}.${path}`) } } else if (!blockConfig.outputs || Object.keys(blockConfig.outputs).length === 0) { - // Handle blocks with no outputs (like starter) - check for custom input fields if (sourceBlock.type === 'starter') { - // Check what start workflow mode is selected - const startWorkflowValue = useSubBlockStore - .getState() - .getValue(activeSourceBlockId, 'startWorkflow') + const startWorkflowValue = getSubBlockValue(activeSourceBlockId, 'startWorkflow') if (startWorkflowValue === 'chat') { - // For chat mode, provide input and conversationId - blockTags = [`${normalizedBlockName}.input`, `${normalizedBlockName}.conversationId`] + // For chat mode, provide input, conversationId, and files + blockTags = [ + `${normalizedBlockName}.input`, + `${normalizedBlockName}.conversationId`, + `${normalizedBlockName}.files`, + ] } else { - // Check for custom input format fields (for manual mode) - const inputFormatValue = useSubBlockStore - .getState() - .getValue(activeSourceBlockId, 'inputFormat') + const inputFormatValue = getSubBlockValue(activeSourceBlockId, 'inputFormat') if ( inputFormatValue && Array.isArray(inputFormatValue) && inputFormatValue.length > 0 ) { - // Use custom input fields if they exist blockTags = inputFormatValue .filter((field: any) => field.name && field.name.trim() !== '') .map((field: any) => `${normalizedBlockName}.${field.name}`) } else { - // Fallback to just the block name blockTags = [normalizedBlockName] } } } else { - // Other blocks with no outputs - show as just blockTags = [normalizedBlockName] } } else { - // Use default block outputs - const outputPaths = generateOutputPaths(blockConfig.outputs || {}) - blockTags = outputPaths.map((path) => `${normalizedBlockName}.${path}`) + if (sourceBlock?.triggerMode && blockConfig.triggers?.enabled) { + const triggers = getTriggersByProvider(sourceBlock.type) + const firstTrigger = triggers[0] + + if (firstTrigger?.outputs) { + // Use trigger outputs instead of block outputs + const outputPaths = generateOutputPaths(firstTrigger.outputs) + blockTags = outputPaths.map((path) => `${normalizedBlockName}.${path}`) + } else { + const outputPaths = generateOutputPaths(blockConfig.outputs || {}) + blockTags = outputPaths.map((path) => `${normalizedBlockName}.${path}`) + } + } else { + // Check for tool-specific outputs first + const operationValue = getSubBlockValue(activeSourceBlockId, 'operation') + const toolOutputPaths = operationValue + ? generateToolOutputPaths(blockConfig, operationValue) + : [] + + if (toolOutputPaths.length > 0) { + blockTags = toolOutputPaths.map((path) => `${normalizedBlockName}.${path}`) + } else { + const outputPaths = generateOutputPaths(blockConfig.outputs || {}) + blockTags = outputPaths.map((path) => `${normalizedBlockName}.${path}`) + } + } } const blockTagGroups: BlockTagGroup[] = [ @@ -261,7 +457,6 @@ export const TagDropdown: React.FC = ({ } } - // Check for invalid blocks before serialization to prevent race conditions const hasInvalidBlocks = Object.values(blocks).some((block) => !block || !block.type) if (hasInvalidBlocks) { return { @@ -271,23 +466,19 @@ export const TagDropdown: React.FC = ({ } } - // Create serialized workflow for BlockPathCalculator const serializer = new Serializer() const serializedWorkflow = serializer.serializeWorkflow(blocks, edges, loops, parallels) - // Find accessible blocks using BlockPathCalculator const accessibleBlockIds = BlockPathCalculator.findAllPathNodes( serializedWorkflow.connections, blockId ) - // Always include starter block const starterBlock = Object.values(blocks).find((block) => block.type === 'starter') if (starterBlock && !accessibleBlockIds.includes(starterBlock.id)) { accessibleBlockIds.push(starterBlock.id) } - // Calculate distances from starter block for ordering const blockDistances: Record = {} if (starterBlock) { const adjList: Record = {} @@ -312,18 +503,17 @@ export const TagDropdown: React.FC = ({ } } - // Create variable tags - filter out variables with empty names const validVariables = workflowVariables.filter( (variable: Variable) => variable.name.trim() !== '' ) const variableTags = validVariables.map( - (variable: Variable) => `variable.${variable.name.replace(/\s+/g, '')}` + (variable: Variable) => `${TAG_PREFIXES.VARIABLE}${normalizeVariableName(variable.name)}` ) const variableInfoMap = validVariables.reduce( (acc, variable) => { - const tagName = `variable.${variable.name.replace(/\s+/g, '')}` + const tagName = `${TAG_PREFIXES.VARIABLE}${normalizeVariableName(variable.name)}` acc[tagName] = { type: variable.type, id: variable.id, @@ -333,7 +523,6 @@ export const TagDropdown: React.FC = ({ {} as Record ) - // Generate loop contextual block group if current block is in a loop let loopBlockGroup: BlockTagGroup | null = null const containingLoop = Object.entries(loops).find(([_, loop]) => loop.nodes.includes(blockId)) let containingLoopBlockId: string | null = null @@ -347,25 +536,22 @@ export const TagDropdown: React.FC = ({ contextualTags.push('items') } - // Add the containing loop block's results to the contextual tags const containingLoopBlock = blocks[loopId] if (containingLoopBlock) { const loopBlockName = containingLoopBlock.name || containingLoopBlock.type - const normalizedLoopBlockName = loopBlockName.replace(/\s+/g, '').toLowerCase() + const normalizedLoopBlockName = normalizeBlockName(loopBlockName) contextualTags.push(`${normalizedLoopBlockName}.results`) - // Create a block group for the loop contextual tags loopBlockGroup = { blockName: loopBlockName, blockId: loopId, blockType: 'loop', tags: contextualTags, - distance: 0, // Contextual tags have highest priority + distance: 0, } } } - // Generate parallel contextual block group if current block is in parallel let parallelBlockGroup: BlockTagGroup | null = null const containingParallel = Object.entries(parallels || {}).find(([_, parallel]) => parallel.nodes.includes(blockId) @@ -376,25 +562,22 @@ export const TagDropdown: React.FC = ({ containingParallelBlockId = parallelId const contextualTags: string[] = ['index', 'currentItem', 'items'] - // Add the containing parallel block's results to the contextual tags const containingParallelBlock = blocks[parallelId] if (containingParallelBlock) { const parallelBlockName = containingParallelBlock.name || containingParallelBlock.type - const normalizedParallelBlockName = parallelBlockName.replace(/\s+/g, '').toLowerCase() + const normalizedParallelBlockName = normalizeBlockName(parallelBlockName) contextualTags.push(`${normalizedParallelBlockName}.results`) - // Create a block group for the parallel contextual tags parallelBlockGroup = { blockName: parallelBlockName, blockId: parallelId, blockType: 'parallel', tags: contextualTags, - distance: 0, // Contextual tags have highest priority + distance: 0, } } } - // Create block tag groups from accessible blocks const blockTagGroups: BlockTagGroup[] = [] const allBlockTags: string[] = [] @@ -404,9 +587,7 @@ export const TagDropdown: React.FC = ({ const blockConfig = getBlock(accessibleBlock.type) - // Handle special blocks that aren't in the registry (loop and parallel) if (!blockConfig) { - // For loop and parallel blocks, create a mock config with results output if (accessibleBlock.type === 'loop' || accessibleBlock.type === 'parallel') { // Skip this block if it's the containing loop/parallel block - we'll handle it with contextual tags if ( @@ -418,13 +599,12 @@ export const TagDropdown: React.FC = ({ const mockConfig = { outputs: { - results: 'array', // These blocks have a results array output + results: 'array', }, } const blockName = accessibleBlock.name || accessibleBlock.type - const normalizedBlockName = blockName.replace(/\s+/g, '').toLowerCase() + const normalizedBlockName = normalizeBlockName(blockName) - // Generate output paths for the mock config const outputPaths = generateOutputPaths(mockConfig.outputs) const blockTags = outputPaths.map((path) => `${normalizedBlockName}.${path}`) @@ -442,81 +622,90 @@ export const TagDropdown: React.FC = ({ } const blockName = accessibleBlock.name || accessibleBlock.type - const normalizedBlockName = blockName.replace(/\s+/g, '').toLowerCase() + const normalizedBlockName = normalizeBlockName(blockName) - // Check for custom response format first - const responseFormatValue = useSubBlockStore - .getState() - .getValue(accessibleBlockId, 'responseFormat') + const responseFormatValue = getSubBlockValue(accessibleBlockId, 'responseFormat') const responseFormat = parseResponseFormatSafely(responseFormatValue, accessibleBlockId) let blockTags: string[] - // Special handling for evaluator blocks if (accessibleBlock.type === 'evaluator') { - // Get the evaluation metrics for the evaluator block - const metricsValue = useSubBlockStore.getState().getValue(accessibleBlockId, 'metrics') + const metricsValue = getSubBlockValue(accessibleBlockId, 'metrics') if (metricsValue && Array.isArray(metricsValue) && metricsValue.length > 0) { - // Use the metric names as the available outputs const validMetrics = metricsValue.filter((metric: any) => metric?.name) blockTags = validMetrics.map( (metric: any) => `${normalizedBlockName}.${metric.name.toLowerCase()}` ) } else { - // Fallback to default evaluator outputs if no metrics are defined const outputPaths = generateOutputPaths(blockConfig.outputs) blockTags = outputPaths.map((path) => `${normalizedBlockName}.${path}`) } } else if (responseFormat) { - // Use custom schema properties if response format is specified const schemaFields = extractFieldsFromSchema(responseFormat) if (schemaFields.length > 0) { blockTags = schemaFields.map((field) => `${normalizedBlockName}.${field.name}`) } else { - // Fallback to default if schema extraction failed const outputPaths = generateOutputPaths(blockConfig.outputs || {}) blockTags = outputPaths.map((path) => `${normalizedBlockName}.${path}`) } } else if (!blockConfig.outputs || Object.keys(blockConfig.outputs).length === 0) { - // Handle blocks with no outputs (like starter) - check for custom input fields if (accessibleBlock.type === 'starter') { - // Check what start workflow mode is selected - const startWorkflowValue = useSubBlockStore - .getState() - .getValue(accessibleBlockId, 'startWorkflow') + const startWorkflowValue = getSubBlockValue(accessibleBlockId, 'startWorkflow') if (startWorkflowValue === 'chat') { - // For chat mode, provide input and conversationId - blockTags = [`${normalizedBlockName}.input`, `${normalizedBlockName}.conversationId`] + // For chat mode, provide input, conversationId, and files + blockTags = [ + `${normalizedBlockName}.input`, + `${normalizedBlockName}.conversationId`, + `${normalizedBlockName}.files`, + ] } else { - // Check for custom input format fields (for manual mode) - const inputFormatValue = useSubBlockStore - .getState() - .getValue(accessibleBlockId, 'inputFormat') + const inputFormatValue = getSubBlockValue(accessibleBlockId, 'inputFormat') if ( inputFormatValue && Array.isArray(inputFormatValue) && inputFormatValue.length > 0 ) { - // Use custom input fields if they exist blockTags = inputFormatValue .filter((field: any) => field.name && field.name.trim() !== '') .map((field: any) => `${normalizedBlockName}.${field.name}`) } else { - // Fallback to just the block name blockTags = [normalizedBlockName] } } } else { - // Other blocks with no outputs - show as just blockTags = [normalizedBlockName] } } else { - // Use default block outputs - const outputPaths = generateOutputPaths(blockConfig.outputs || {}) - blockTags = outputPaths.map((path) => `${normalizedBlockName}.${path}`) + const blockState = blocks[accessibleBlockId] + if (blockState?.triggerMode && blockConfig.triggers?.enabled) { + const triggers = getTriggersByProvider(blockState.type) // Use block type as provider + const firstTrigger = triggers[0] + + if (firstTrigger?.outputs) { + // Use trigger outputs instead of block outputs + const outputPaths = generateOutputPaths(firstTrigger.outputs) + blockTags = outputPaths.map((path) => `${normalizedBlockName}.${path}`) + } else { + const outputPaths = generateOutputPaths(blockConfig.outputs || {}) + blockTags = outputPaths.map((path) => `${normalizedBlockName}.${path}`) + } + } else { + // Check for tool-specific outputs first + const operationValue = getSubBlockValue(accessibleBlockId, 'operation') + const toolOutputPaths = operationValue + ? generateToolOutputPaths(blockConfig, operationValue) + : [] + + if (toolOutputPaths.length > 0) { + blockTags = toolOutputPaths.map((path) => `${normalizedBlockName}.${path}`) + } else { + const outputPaths = generateOutputPaths(blockConfig.outputs || {}) + blockTags = outputPaths.map((path) => `${normalizedBlockName}.${path}`) + } + } } blockTagGroups.push({ @@ -530,7 +719,6 @@ export const TagDropdown: React.FC = ({ allBlockTags.push(...blockTags) } - // Add contextual block groups at the beginning (they have highest priority) const finalBlockTagGroups: BlockTagGroup[] = [] if (loopBlockGroup) { finalBlockTagGroups.push(loopBlockGroup) @@ -539,11 +727,9 @@ export const TagDropdown: React.FC = ({ finalBlockTagGroups.push(parallelBlockGroup) } - // Sort regular block groups by distance (closest first) and add them blockTagGroups.sort((a, b) => a.distance - b.distance) finalBlockTagGroups.push(...blockTagGroups) - // Collect all tags for the main tags array const contextualTags: string[] = [] if (loopBlockGroup) { contextualTags.push(...loopBlockGroup.tags) @@ -559,23 +745,20 @@ export const TagDropdown: React.FC = ({ } }, [blocks, edges, loops, parallels, blockId, activeSourceBlockId, workflowVariables]) - // Filter tags based on search term const filteredTags = useMemo(() => { if (!searchTerm) return tags return tags.filter((tag: string) => tag.toLowerCase().includes(searchTerm)) }, [tags, searchTerm]) - // Group filtered tags by category const { variableTags, filteredBlockTagGroups } = useMemo(() => { const varTags: string[] = [] filteredTags.forEach((tag) => { - if (tag.startsWith('variable.')) { + if (tag.startsWith(TAG_PREFIXES.VARIABLE)) { varTags.push(tag) } }) - // Filter block tag groups based on search term const filteredBlockTagGroups = blockTagGroups .map((group) => ({ ...group, @@ -589,13 +772,85 @@ export const TagDropdown: React.FC = ({ } }, [filteredTags, blockTagGroups, searchTerm]) - // Create ordered tags for keyboard navigation + const nestedBlockTagGroups = useMemo(() => { + return filteredBlockTagGroups.map((group) => { + const nestedTags: Array<{ + key: string + display: string + fullTag?: string + children?: Array<{ key: string; display: string; fullTag: string }> + }> = [] + + const groupedTags: Record< + string, + Array<{ key: string; display: string; fullTag: string }> + > = {} + const directTags: Array<{ key: string; display: string; fullTag: string }> = [] + + group.tags.forEach((tag) => { + const tagParts = tag.split('.') + if (tagParts.length >= 3) { + const parent = tagParts[1] + const child = tagParts.slice(2).join('.') + + if (!groupedTags[parent]) { + groupedTags[parent] = [] + } + groupedTags[parent].push({ + key: `${parent}.${child}`, + display: child, + fullTag: tag, + }) + } else { + const path = tagParts.slice(1).join('.') + directTags.push({ + key: path || group.blockName, + display: path || group.blockName, + fullTag: tag, + }) + } + }) + + Object.entries(groupedTags).forEach(([parent, children]) => { + nestedTags.push({ + key: parent, + display: parent, + children: children, + }) + }) + + directTags.forEach((directTag) => { + nestedTags.push(directTag) + }) + + return { + ...group, + nestedTags, + } + }) + }, [filteredBlockTagGroups]) + const orderedTags = useMemo(() => { - const allBlockTags = filteredBlockTagGroups.flatMap((group) => group.tags) - return [...variableTags, ...allBlockTags] - }, [variableTags, filteredBlockTagGroups]) + const visualTags: string[] = [] + + visualTags.push(...variableTags) + + nestedBlockTagGroups.forEach((group) => { + group.nestedTags.forEach((nestedTag) => { + if (nestedTag.children && nestedTag.children.length > 0) { + const firstChild = nestedTag.children[0] + if (firstChild.fullTag) { + visualTags.push(firstChild.fullTag) + } + } else if (nestedTag.fullTag) { + visualTags.push(nestedTag.fullTag) + } + }) + }) + + return visualTags + }, [variableTags, nestedBlockTagGroups]) - // Create efficient tag index lookup map const tagIndexMap = useMemo(() => { const map = new Map() orderedTags.forEach((tag, index) => { @@ -604,22 +859,18 @@ export const TagDropdown: React.FC = ({ return map }, [orderedTags]) - // Handle tag selection and text replacement const handleTagSelect = useCallback( (tag: string, blockGroup?: BlockTagGroup) => { const textBeforeCursor = inputValue.slice(0, cursorPosition) const textAfterCursor = inputValue.slice(cursorPosition) - // Find the position of the last '<' before cursor const lastOpenBracket = textBeforeCursor.lastIndexOf('<') if (lastOpenBracket === -1) return - // Process different types of tags let processedTag = tag - // Handle variable tags - if (tag.startsWith('variable.')) { - const variableName = tag.substring('variable.'.length) + if (tag.startsWith(TAG_PREFIXES.VARIABLE)) { + const variableName = tag.substring(TAG_PREFIXES.VARIABLE.length) const variableObj = Object.values(variables).find( (v) => v.name.replace(/\s+/g, '') === variableName ) @@ -627,28 +878,22 @@ export const TagDropdown: React.FC = ({ if (variableObj) { processedTag = tag } - } - // Handle contextual loop/parallel tags - else if ( + } else if ( blockGroup && (blockGroup.blockType === 'loop' || blockGroup.blockType === 'parallel') ) { - // Check if this is a contextual tag (without dots) that needs a prefix if (!tag.includes('.') && ['index', 'currentItem', 'items'].includes(tag)) { processedTag = `${blockGroup.blockType}.${tag}` } else { - // It's already a properly formatted tag (like blockname.results) processedTag = tag } } - // Handle existing closing bracket const nextCloseBracket = textAfterCursor.indexOf('>') let remainingTextAfterCursor = textAfterCursor if (nextCloseBracket !== -1) { const textBetween = textAfterCursor.slice(0, nextCloseBracket) - // If text between cursor and '>' contains only tag-like characters, skip it if (/^[a-zA-Z0-9._]*$/.test(textBetween)) { remainingTextAfterCursor = textAfterCursor.slice(nextCloseBracket + 1) } @@ -662,65 +907,240 @@ export const TagDropdown: React.FC = ({ [inputValue, cursorPosition, variables, onSelect, onClose] ) - // Reset selection when search results change useEffect(() => setSelectedIndex(0), [searchTerm]) - // Keep selection within bounds when tags change useEffect(() => { if (selectedIndex >= orderedTags.length) { setSelectedIndex(Math.max(0, orderedTags.length - 1)) } }, [orderedTags.length, selectedIndex]) - // Handle keyboard navigation useEffect(() => { if (visible) { const handleKeyboardEvent = (e: KeyboardEvent) => { if (!orderedTags.length) return - switch (e.key) { - case 'ArrowDown': - e.preventDefault() - e.stopPropagation() - setSelectedIndex((prev) => Math.min(prev + 1, orderedTags.length - 1)) - break - case 'ArrowUp': - e.preventDefault() - e.stopPropagation() - setSelectedIndex((prev) => Math.max(prev - 1, 0)) - break - case 'Enter': - e.preventDefault() - e.stopPropagation() - if (selectedIndex >= 0 && selectedIndex < orderedTags.length) { - const selectedTag = orderedTags[selectedIndex] - // Find which block group this tag belongs to - const belongsToGroup = filteredBlockTagGroups.find((group) => - group.tags.includes(selectedTag) - ) - handleTagSelect(selectedTag, belongsToGroup) - } - break - case 'Escape': - e.preventDefault() - e.stopPropagation() - onClose?.() - break + if (inSubmenu) { + const currentHovered = hoveredNested + if (!currentHovered) { + setInSubmenu(false) + return + } + + const currentGroup = nestedBlockTagGroups.find((group) => { + return group.nestedTags.some( + (tag, index) => + `${group.blockId}-${tag.key}` === currentHovered.tag && + index === currentHovered.index + ) + }) + + const currentNestedTag = currentGroup?.nestedTags.find( + (tag, index) => + `${currentGroup.blockId}-${tag.key}` === currentHovered.tag && + index === currentHovered.index + ) + + const children = currentNestedTag?.children || [] + + switch (e.key) { + case 'ArrowDown': + e.preventDefault() + e.stopPropagation() + setSubmenuIndex((prev) => Math.min(prev + 1, children.length - 1)) + break + case 'ArrowUp': + e.preventDefault() + e.stopPropagation() + setSubmenuIndex((prev) => Math.max(prev - 1, 0)) + break + case 'ArrowLeft': + e.preventDefault() + e.stopPropagation() + setInSubmenu(false) + setHoveredNested(null) + setSubmenuIndex(0) + break + case 'Enter': + e.preventDefault() + e.stopPropagation() + if (submenuIndex >= 0 && submenuIndex < children.length) { + const selectedChild = children[submenuIndex] + handleTagSelect(selectedChild.fullTag, currentGroup) + } + break + case 'Escape': + e.preventDefault() + e.stopPropagation() + setInSubmenu(false) + setHoveredNested(null) + setSubmenuIndex(0) + break + } + } else { + switch (e.key) { + case 'ArrowDown': + e.preventDefault() + e.stopPropagation() + setSelectedIndex((prev) => { + const newIndex = Math.min(prev + 1, orderedTags.length - 1) + const newSelectedTag = orderedTags[newIndex] + let foundParent = false + for (const group of nestedBlockTagGroups) { + for ( + let nestedTagIndex = 0; + nestedTagIndex < group.nestedTags.length; + nestedTagIndex++ + ) { + const nestedTag = group.nestedTags[nestedTagIndex] + if (nestedTag.children && nestedTag.children.length > 0) { + const firstChild = nestedTag.children[0] + if (firstChild.fullTag === newSelectedTag) { + setHoveredNested({ + tag: `${group.blockId}-${nestedTag.key}`, + index: nestedTagIndex, + }) + foundParent = true + break + } + } + } + if (foundParent) break + } + if (!foundParent && !inSubmenu) { + setHoveredNested(null) + } + return newIndex + }) + break + case 'ArrowUp': + e.preventDefault() + e.stopPropagation() + setSelectedIndex((prev) => { + const newIndex = Math.max(prev - 1, 0) + const newSelectedTag = orderedTags[newIndex] + let foundParent = false + for (const group of nestedBlockTagGroups) { + for ( + let nestedTagIndex = 0; + nestedTagIndex < group.nestedTags.length; + nestedTagIndex++ + ) { + const nestedTag = group.nestedTags[nestedTagIndex] + if (nestedTag.children && nestedTag.children.length > 0) { + const firstChild = nestedTag.children[0] + if (firstChild.fullTag === newSelectedTag) { + setHoveredNested({ + tag: `${group.blockId}-${nestedTag.key}`, + index: nestedTagIndex, + }) + foundParent = true + break + } + } + } + if (foundParent) break + } + if (!foundParent && !inSubmenu) { + setHoveredNested(null) + } + return newIndex + }) + break + case 'ArrowRight': + e.preventDefault() + e.stopPropagation() + if (selectedIndex >= 0 && selectedIndex < orderedTags.length) { + const selectedTag = orderedTags[selectedIndex] + for (const group of nestedBlockTagGroups) { + for ( + let nestedTagIndex = 0; + nestedTagIndex < group.nestedTags.length; + nestedTagIndex++ + ) { + const nestedTag = group.nestedTags[nestedTagIndex] + if (nestedTag.children && nestedTag.children.length > 0) { + const firstChild = nestedTag.children[0] + if (firstChild.fullTag === selectedTag) { + setInSubmenu(true) + setSubmenuIndex(0) + setHoveredNested({ + tag: `${group.blockId}-${nestedTag.key}`, + index: nestedTagIndex, + }) + return + } + } + } + } + } + break + case 'Enter': + e.preventDefault() + e.stopPropagation() + if (selectedIndex >= 0 && selectedIndex < orderedTags.length) { + const selectedTag = orderedTags[selectedIndex] + + let isParentItem = false + let parentTag = '' + let parentGroup: BlockTagGroup | undefined + + for (const group of nestedBlockTagGroups) { + for (const nestedTag of group.nestedTags) { + if (nestedTag.children && nestedTag.children.length > 0) { + const firstChild = nestedTag.children[0] + if (firstChild.fullTag === selectedTag) { + isParentItem = true + parentTag = `${normalizeBlockName(group.blockName)}.${nestedTag.key}` + parentGroup = group + break + } + } + } + if (isParentItem) break + } + + if (isParentItem && parentTag) { + handleTagSelect(parentTag, parentGroup) + } else { + const belongsToGroup = filteredBlockTagGroups.find((group) => + group.tags.includes(selectedTag) + ) + handleTagSelect(selectedTag, belongsToGroup) + } + } + break + case 'Escape': + e.preventDefault() + e.stopPropagation() + onClose?.() + break + } } } window.addEventListener('keydown', handleKeyboardEvent, true) return () => window.removeEventListener('keydown', handleKeyboardEvent, true) } - }, [visible, selectedIndex, orderedTags, filteredBlockTagGroups, handleTagSelect, onClose]) + }, [ + visible, + selectedIndex, + orderedTags, + filteredBlockTagGroups, + nestedBlockTagGroups, + handleTagSelect, + onClose, + inSubmenu, + submenuIndex, + hoveredNested, + ]) - // Early return if dropdown should not be visible if (!visible || tags.length === 0 || orderedTags.length === 0) return null return (
= ({ tagIndex >= 0 && 'bg-accent text-accent-foreground' )} - onMouseEnter={() => setSelectedIndex(tagIndex >= 0 ? tagIndex : 0)} - onMouseDown={(e) => { - e.preventDefault() - e.stopPropagation() - handleTagSelect(tag) - }} - onClick={(e) => { - e.preventDefault() - e.stopPropagation() - handleTagSelect(tag) - }} + {...createTagEventHandlers( + tag, + undefined, + tagIndex, + handleTagSelect, + setSelectedIndex, + setHoveredNested + )} >
V
- {tag.startsWith('variable.') ? tag.substring('variable.'.length) : tag} + {tag.startsWith(TAG_PREFIXES.VARIABLE) + ? tag.substring(TAG_PREFIXES.VARIABLE.length) + : tag} {variableInfo && ( @@ -785,100 +1204,261 @@ export const TagDropdown: React.FC = ({ )} - {/* Block sections */} - {filteredBlockTagGroups.length > 0 && ( + {/* Block sections with nested structure */} + {nestedBlockTagGroups.length > 0 && ( <> {variableTags.length > 0 &&
} - {filteredBlockTagGroups.map((group) => { - // Get block color from configuration + {nestedBlockTagGroups.map((group) => { const blockConfig = getBlock(group.blockType) - let blockColor = blockConfig?.bgColor || '#2F55FF' + let blockColor = blockConfig?.bgColor || BLOCK_COLORS.DEFAULT - // Handle special colors for loop and parallel blocks if (group.blockType === 'loop') { - blockColor = '#8857E6' // Purple color for loop blocks + blockColor = BLOCK_COLORS.LOOP } else if (group.blockType === 'parallel') { - blockColor = '#FF5757' // Red color for parallel blocks + blockColor = BLOCK_COLORS.PARALLEL } return ( -
+
{group.blockName}
- {group.tags.map((tag: string) => { - const tagIndex = tagIndexMap.get(tag) ?? -1 - - // Handle display text based on tag type - let displayText: string + {group.nestedTags.map((nestedTag, index) => { + const tagIndex = nestedTag.fullTag + ? (tagIndexMap.get(nestedTag.fullTag) ?? -1) + : -1 + const hasChildren = nestedTag.children && nestedTag.children.length > 0 + const isHovered = + hoveredNested?.tag === `${group.blockId}-${nestedTag.key}` && + hoveredNested?.index === index + + const displayText = nestedTag.display let tagDescription = '' let tagIcon = group.blockName.charAt(0).toUpperCase() if ( (group.blockType === 'loop' || group.blockType === 'parallel') && - !tag.includes('.') + !nestedTag.key.includes('.') ) { - // Contextual tags like 'index', 'currentItem', 'items' - displayText = tag - if (tag === 'index') { + if (nestedTag.key === 'index') { tagIcon = '#' - tagDescription = 'Index' - } else if (tag === 'currentItem') { + tagDescription = 'number' + } else if (nestedTag.key === 'currentItem') { tagIcon = 'i' - tagDescription = 'Current item' - } else if (tag === 'items') { + tagDescription = 'any' + } else if (nestedTag.key === 'items') { tagIcon = 'I' - tagDescription = 'All items' + tagDescription = 'array' } } else { - // Regular block output tags like 'blockname.field' or 'blockname.results' - const tagParts = tag.split('.') - const path = tagParts.slice(1).join('.') - displayText = path || group.blockName - if (path === 'results') { - tagDescription = 'Results array' + if (nestedTag.fullTag) { + const tagParts = nestedTag.fullTag.split('.') + const outputPath = tagParts.slice(1).join('.') + + const block = Object.values(blocks).find( + (b) => b.id === group.blockId + ) + if (block) { + const blockConfig = getBlock(block.type) + + tagDescription = getOutputTypeForPath( + block, + blockConfig, + group.blockId, + outputPath + ) + } } } + const isKeyboardSelected = (() => { + if ( + hasChildren && + selectedIndex >= 0 && + selectedIndex < orderedTags.length + ) { + const selectedTag = orderedTags[selectedIndex] + const firstChild = nestedTag.children?.[0] + return firstChild?.fullTag === selectedTag + } + return tagIndex === selectedIndex && tagIndex >= 0 + })() + return ( - + + {/* Nested submenu */} + {hasChildren && isHovered && ( +
{ + setSubmenuHovered(true) + const parentKey = `${group.blockId}-${nestedTag.key}` + setHoveredNested({ + tag: parentKey, + index, + }) + setSubmenuIndex(-1) + }} + onMouseLeave={() => { + setSubmenuHovered(false) + const parentKey = `${group.blockId}-${nestedTag.key}` + if (parentHovered !== parentKey) { + setHoveredNested(null) + } + }} + > +
+ {nestedTag.children!.map((child, childIndex) => { + const isKeyboardSelected = + inSubmenu && submenuIndex === childIndex + const isSelected = isKeyboardSelected + + let childType = '' + const childTagParts = child.fullTag.split('.') + const childOutputPath = childTagParts.slice(1).join('.') + + const block = Object.values(blocks).find( + (b) => b.id === group.blockId + ) + if (block) { + const blockConfig = getBlock(block.type) + + childType = getOutputTypeForPath( + block, + blockConfig, + group.blockId, + childOutputPath + ) + } + + return ( + + ) + })} +
+
)} - +
) })}
diff --git a/apps/sim/db/migrations/0041_common_doomsday.sql b/apps/sim/db/migrations/0041_common_doomsday.sql deleted file mode 100644 index c43f77500e..0000000000 --- a/apps/sim/db/migrations/0041_common_doomsday.sql +++ /dev/null @@ -1,5 +0,0 @@ -ALTER TABLE "document" ADD COLUMN "processing_status" text DEFAULT 'pending' NOT NULL;--> statement-breakpoint -ALTER TABLE "document" ADD COLUMN "processing_started_at" timestamp;--> statement-breakpoint -ALTER TABLE "document" ADD COLUMN "processing_completed_at" timestamp;--> statement-breakpoint -ALTER TABLE "document" ADD COLUMN "processing_error" text;--> statement-breakpoint -CREATE INDEX "doc_processing_status_idx" ON "document" USING btree ("knowledge_base_id","processing_status"); \ No newline at end of file diff --git a/apps/sim/db/migrations/0068_fine_hardball.sql b/apps/sim/db/migrations/0068_fine_hardball.sql new file mode 100644 index 0000000000..a96c62397a --- /dev/null +++ b/apps/sim/db/migrations/0068_fine_hardball.sql @@ -0,0 +1 @@ +ALTER TABLE "workflow_execution_logs" ADD COLUMN "files" jsonb; \ No newline at end of file diff --git a/apps/sim/db/migrations/0069_lonely_spirit.sql b/apps/sim/db/migrations/0069_lonely_spirit.sql new file mode 100644 index 0000000000..2fa0a6aa53 --- /dev/null +++ b/apps/sim/db/migrations/0069_lonely_spirit.sql @@ -0,0 +1 @@ +ALTER TABLE "workflow_blocks" ADD COLUMN "trigger_mode" boolean DEFAULT false NOT NULL; \ No newline at end of file diff --git a/apps/sim/db/migrations/0070_charming_wrecking_crew.sql b/apps/sim/db/migrations/0070_charming_wrecking_crew.sql new file mode 100644 index 0000000000..e66c2324cb --- /dev/null +++ b/apps/sim/db/migrations/0070_charming_wrecking_crew.sql @@ -0,0 +1 @@ +ALTER TABLE "knowledge_base" ALTER COLUMN "chunking_config" SET DEFAULT '{"maxSize": 1024, "minSize": 1, "overlap": 200}'; \ No newline at end of file diff --git a/apps/sim/db/migrations/meta/0068_snapshot.json b/apps/sim/db/migrations/meta/0068_snapshot.json new file mode 100644 index 0000000000..f507186626 --- /dev/null +++ b/apps/sim/db/migrations/meta/0068_snapshot.json @@ -0,0 +1,5856 @@ +{ + "id": "28799b2c-5dc3-4b16-a5a1-886b68d54fd3", + "prevId": "141c3409-4c33-42e4-a5f5-e95feea9c10e", + "version": "7", + "dialect": "postgresql", + "tables": { + "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": {}, + "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 + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "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": {}, + "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" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "api_key_key_unique": { + "name": "api_key_key_unique", + "nullsNotDistinct": false, + "columns": ["key"] + } + }, + "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 + }, + "subdomain": { + "name": "subdomain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "customizations": { + "name": "customizations", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'public'" + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_emails": { + "name": "allowed_emails", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "output_configs": { + "name": "output_configs", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "subdomain_idx": { + "name": "subdomain_idx", + "columns": [ + { + "expression": "subdomain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_workflow_id_workflow_id_fk": { + "name": "chat_workflow_id_workflow_id_fk", + "tableFrom": "chat", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_user_id_user_id_fk": { + "name": "chat_user_id_user_id_fk", + "tableFrom": "chat", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_chats": { + "name": "copilot_chats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "messages": { + "name": "messages", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'claude-3-7-sonnet-latest'" + }, + "preview_yaml": { + "name": "preview_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_chats_user_id_idx": { + "name": "copilot_chats_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_workflow_id_idx": { + "name": "copilot_chats_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_workflow_idx": { + "name": "copilot_chats_user_workflow_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_created_at_idx": { + "name": "copilot_chats_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_updated_at_idx": { + "name": "copilot_chats_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_chats_user_id_user_id_fk": { + "name": "copilot_chats_user_id_user_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_chats_workflow_id_workflow_id_fk": { + "name": "copilot_chats_workflow_id_workflow_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_feedback": { + "name": "copilot_feedback", + "schema": "", + "columns": { + "feedback_id": { + "name": "feedback_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_query": { + "name": "user_query", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_response": { + "name": "agent_response", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_positive": { + "name": "is_positive", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "feedback": { + "name": "feedback", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_yaml": { + "name": "workflow_yaml", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_feedback_user_id_idx": { + "name": "copilot_feedback_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_chat_id_idx": { + "name": "copilot_feedback_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_user_chat_idx": { + "name": "copilot_feedback_user_chat_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_is_positive_idx": { + "name": "copilot_feedback_is_positive_idx", + "columns": [ + { + "expression": "is_positive", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_created_at_idx": { + "name": "copilot_feedback_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_feedback_user_id_user_id_fk": { + "name": "copilot_feedback_user_id_user_id_fk", + "tableFrom": "copilot_feedback", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_feedback_chat_id_copilot_chats_id_fk": { + "name": "copilot_feedback_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_feedback", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.custom_tools": { + "name": "custom_tools", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "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": {}, + "foreignKeys": { + "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": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.docs_embeddings": { + "name": "docs_embeddings", + "schema": "", + "columns": { + "chunk_id": { + "name": "chunk_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chunk_text": { + "name": "chunk_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_document": { + "name": "source_document", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_link": { + "name": "source_link", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "header_text": { + "name": "header_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "header_level": { + "name": "header_level", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": true + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "chunk_text_tsv": { + "name": "chunk_text_tsv", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "to_tsvector('english', \"docs_embeddings\".\"chunk_text\")", + "type": "stored" + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "docs_emb_source_document_idx": { + "name": "docs_emb_source_document_idx", + "columns": [ + { + "expression": "source_document", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_header_level_idx": { + "name": "docs_emb_header_level_idx", + "columns": [ + { + "expression": "header_level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_source_header_idx": { + "name": "docs_emb_source_header_idx", + "columns": [ + { + "expression": "source_document", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "header_level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_model_idx": { + "name": "docs_emb_model_idx", + "columns": [ + { + "expression": "embedding_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_created_at_idx": { + "name": "docs_emb_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_embedding_vector_hnsw_idx": { + "name": "docs_embedding_vector_hnsw_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "docs_emb_metadata_gin_idx": { + "name": "docs_emb_metadata_gin_idx", + "columns": [ + { + "expression": "metadata", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "docs_emb_chunk_text_fts_idx": { + "name": "docs_emb_chunk_text_fts_idx", + "columns": [ + { + "expression": "chunk_text_tsv", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "docs_embedding_not_null_check": { + "name": "docs_embedding_not_null_check", + "value": "\"embedding\" IS NOT NULL" + }, + "docs_header_level_check": { + "name": "docs_header_level_check", + "value": "\"header_level\" >= 1 AND \"header_level\" <= 6" + } + }, + "isRLSEnabled": false + }, + "public.document": { + "name": "document", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_url": { + "name": "file_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_size": { + "name": "file_size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_count": { + "name": "chunk_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "character_count": { + "name": "character_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "processing_status": { + "name": "processing_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "processing_started_at": { + "name": "processing_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_completed_at": { + "name": "processing_completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_error": { + "name": "processing_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "tag1": { + "name": "tag1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag2": { + "name": "tag2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag3": { + "name": "tag3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag4": { + "name": "tag4", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag5": { + "name": "tag5", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag6": { + "name": "tag6", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag7": { + "name": "tag7", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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_kb_uploaded_at_idx": { + "name": "doc_kb_uploaded_at_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "uploaded_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_processing_status_idx": { + "name": "doc_processing_status_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "processing_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag1_idx": { + "name": "doc_tag1_idx", + "columns": [ + { + "expression": "tag1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag2_idx": { + "name": "doc_tag2_idx", + "columns": [ + { + "expression": "tag2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag3_idx": { + "name": "doc_tag3_idx", + "columns": [ + { + "expression": "tag3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag4_idx": { + "name": "doc_tag4_idx", + "columns": [ + { + "expression": "tag4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag5_idx": { + "name": "doc_tag5_idx", + "columns": [ + { + "expression": "tag5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag6_idx": { + "name": "doc_tag6_idx", + "columns": [ + { + "expression": "tag6", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag7_idx": { + "name": "doc_tag7_idx", + "columns": [ + { + "expression": "tag7", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "document_knowledge_base_id_knowledge_base_id_fk": { + "name": "document_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "document", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.embedding": { + "name": "embedding", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_index": { + "name": "chunk_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "chunk_hash": { + "name": "chunk_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_length": { + "name": "content_length", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": false + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "start_offset": { + "name": "start_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "end_offset": { + "name": "end_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "tag1": { + "name": "tag1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag2": { + "name": "tag2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag3": { + "name": "tag3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag4": { + "name": "tag4", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag5": { + "name": "tag5", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag6": { + "name": "tag6", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag7": { + "name": "tag7", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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_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.invitation": { + "name": "invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "invitation_inviter_id_user_id_fk": { + "name": "invitation_inviter_id_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "columnsFrom": ["inviter_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_base": { + "name": "knowledge_base", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "embedding_dimension": { + "name": "embedding_dimension", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1536 + }, + "chunking_config": { + "name": "chunking_config", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{\"maxSize\": 1024, \"minSize\": 100, \"overlap\": 200}'" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kb_user_id_idx": { + "name": "kb_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_id_idx": { + "name": "kb_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_user_workspace_idx": { + "name": "kb_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_deleted_at_idx": { + "name": "kb_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_base_user_id_user_id_fk": { + "name": "knowledge_base_user_id_user_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_base_workspace_id_workspace_id_fk": { + "name": "knowledge_base_workspace_id_workspace_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_base_tag_definitions": { + "name": "knowledge_base_tag_definitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tag_slot": { + "name": "tag_slot", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "field_type": { + "name": "field_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kb_tag_definitions_kb_slot_idx": { + "name": "kb_tag_definitions_kb_slot_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tag_slot", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_tag_definitions_kb_display_name_idx": { + "name": "kb_tag_definitions_kb_display_name_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "display_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_tag_definitions_kb_id_idx": { + "name": "kb_tag_definitions_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk": { + "name": "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "knowledge_base_tag_definitions", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.marketplace": { + "name": "marketplace", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "author_id": { + "name": "author_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author_name": { + "name": "author_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "views": { + "name": "views", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "category": { + "name": "category", + "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": {}, + "foreignKeys": { + "marketplace_workflow_id_workflow_id_fk": { + "name": "marketplace_workflow_id_workflow_id_fk", + "tableFrom": "marketplace", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "marketplace_author_id_user_id_fk": { + "name": "marketplace_author_id_user_id_fk", + "tableFrom": "marketplace", + "tableTo": "user", + "columnsFrom": ["author_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "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": {}, + "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 + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "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()" + }, + "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_workflow_idx": { + "name": "memory_workflow_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workflow_key_idx": { + "name": "memory_workflow_key_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "memory_workflow_id_workflow_id_fk": { + "name": "memory_workflow_id_workflow_id_fk", + "tableFrom": "memory", + "tableTo": "workflow", + "columnsFrom": ["workflow_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 + }, + "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.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.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "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 + }, + "auto_fill_env_vars": { + "name": "auto_fill_env_vars", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "auto_pan": { + "name": "auto_pan", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "console_expanded_by_default": { + "name": "console_expanded_by_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "telemetry_enabled": { + "name": "telemetry_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "telemetry_notified_user": { + "name": "telemetry_notified_user", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "email_preferences": { + "name": "email_preferences", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "general": { + "name": "general", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "settings_user_id_user_id_fk": { + "name": "settings_user_id_user_id_fk", + "tableFrom": "settings", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "settings_user_id_unique": { + "name": "settings_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.subscription": { + "name": "subscription", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "plan": { + "name": "plan", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "period_start": { + "name": "period_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "period_end": { + "name": "period_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "cancel_at_period_end": { + "name": "cancel_at_period_end", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "seats": { + "name": "seats", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "trial_start": { + "name": "trial_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "trial_end": { + "name": "trial_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "subscription_reference_status_idx": { + "name": "subscription_reference_status_idx", + "columns": [ + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "check_enterprise_metadata": { + "name": "check_enterprise_metadata", + "value": "plan != 'enterprise' OR (metadata IS NOT NULL AND (metadata->>'perSeatAllowance' IS NOT NULL OR metadata->>'totalAllowance' IS NOT NULL))" + } + }, + "isRLSEnabled": false + }, + "public.template_stars": { + "name": "template_stars", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "template_id": { + "name": "template_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "starred_at": { + "name": "starred_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "template_stars_user_id_idx": { + "name": "template_stars_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_template_id_idx": { + "name": "template_stars_template_id_idx", + "columns": [ + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_user_template_idx": { + "name": "template_stars_user_template_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_template_user_idx": { + "name": "template_stars_template_user_idx", + "columns": [ + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_starred_at_idx": { + "name": "template_stars_starred_at_idx", + "columns": [ + { + "expression": "starred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_template_starred_at_idx": { + "name": "template_stars_template_starred_at_idx", + "columns": [ + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "starred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_user_template_unique": { + "name": "template_stars_user_template_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "template_stars_user_id_user_id_fk": { + "name": "template_stars_user_id_user_id_fk", + "tableFrom": "template_stars", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "template_stars_template_id_templates_id_fk": { + "name": "template_stars_template_id_templates_id_fk", + "tableFrom": "template_stars", + "tableTo": "templates", + "columnsFrom": ["template_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.templates": { + "name": "templates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_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 + }, + "author": { + "name": "author", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "views": { + "name": "views", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "stars": { + "name": "stars", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'#3972F6'" + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'FileText'" + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "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()" + } + }, + "indexes": { + "templates_workflow_id_idx": { + "name": "templates_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_user_id_idx": { + "name": "templates_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_category_idx": { + "name": "templates_category_idx", + "columns": [ + { + "expression": "category", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_views_idx": { + "name": "templates_views_idx", + "columns": [ + { + "expression": "views", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_stars_idx": { + "name": "templates_stars_idx", + "columns": [ + { + "expression": "stars", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_category_views_idx": { + "name": "templates_category_views_idx", + "columns": [ + { + "expression": "category", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "views", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_category_stars_idx": { + "name": "templates_category_stars_idx", + "columns": [ + { + "expression": "category", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "stars", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_user_category_idx": { + "name": "templates_user_category_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "category", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_created_at_idx": { + "name": "templates_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_updated_at_idx": { + "name": "templates_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "templates_workflow_id_workflow_id_fk": { + "name": "templates_workflow_id_workflow_id_fk", + "tableFrom": "templates", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "templates_user_id_user_id_fk": { + "name": "templates_user_id_user_id_fk", + "tableFrom": "templates", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_rate_limits": { + "name": "user_rate_limits", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "sync_api_requests": { + "name": "sync_api_requests", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "async_api_requests": { + "name": "async_api_requests", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "window_start": { + "name": "window_start", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_request_at": { + "name": "last_request_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "is_rate_limited": { + "name": "is_rate_limited", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "rate_limit_reset_at": { + "name": "rate_limit_reset_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_rate_limits_user_id_user_id_fk": { + "name": "user_rate_limits_user_id_user_id_fk", + "tableFrom": "user_rate_limits", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_stats": { + "name": "user_stats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "total_manual_executions": { + "name": "total_manual_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_api_calls": { + "name": "total_api_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_webhook_triggers": { + "name": "total_webhook_triggers", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_scheduled_executions": { + "name": "total_scheduled_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_chat_executions": { + "name": "total_chat_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_tokens_used": { + "name": "total_tokens_used", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_cost": { + "name": "total_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_usage_limit": { + "name": "current_usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'10'" + }, + "usage_limit_set_by": { + "name": "usage_limit_set_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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'" + }, + "billing_period_start": { + "name": "billing_period_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "billing_period_end": { + "name": "billing_period_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_period_cost": { + "name": "last_period_cost", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "last_active": { + "name": "last_active", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_stats_user_id_user_id_fk": { + "name": "user_stats_user_id_user_id_fk", + "tableFrom": "user_stats", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_stats_user_id_unique": { + "name": "user_stats_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.waitlist": { + "name": "waitlist", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "waitlist_email_unique": { + "name": "waitlist_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook": { + "name": "webhook", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_config": { + "name": "provider_config", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "path_idx": { + "name": "path_idx", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_workflow_id_workflow_id_fk": { + "name": "webhook_workflow_id_workflow_id_fk", + "tableFrom": "webhook", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webhook_block_id_workflow_blocks_id_fk": { + "name": "webhook_block_id_workflow_blocks_id_fk", + "tableFrom": "webhook", + "tableTo": "workflow_blocks", + "columnsFrom": ["block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow": { + "name": "workflow", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'#3972F6'" + }, + "last_synced": { + "name": "last_synced", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "is_deployed": { + "name": "is_deployed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deployed_state": { + "name": "deployed_state", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "deployed_at": { + "name": "deployed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "collaborators": { + "name": "collaborators", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "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": "'{}'" + }, + "is_published": { + "name": "is_published", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "marketplace_data": { + "name": "marketplace_data", + "type": "json", + "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": {} + } + }, + "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 + }, + "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": "'{}'" + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "extent": { + "name": "extent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_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_parent_id_idx": { + "name": "workflow_blocks_parent_id_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_blocks_workflow_parent_idx": { + "name": "workflow_blocks_workflow_parent_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_blocks_workflow_type_idx": { + "name": "workflow_blocks_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_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_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_source_block_idx": { + "name": "workflow_edges_source_block_idx", + "columns": [ + { + "expression": "source_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_target_block_idx": { + "name": "workflow_edges_target_block_idx", + "columns": [ + { + "expression": "target_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_workflow_source_idx": { + "name": "workflow_edges_workflow_source_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_workflow_target_idx": { + "name": "workflow_edges_workflow_target_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_edges_workflow_id_workflow_id_fk": { + "name": "workflow_edges_workflow_id_workflow_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_edges_source_block_id_workflow_blocks_id_fk": { + "name": "workflow_edges_source_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow_blocks", + "columnsFrom": ["source_block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_edges_target_block_id_workflow_blocks_id_fk": { + "name": "workflow_edges_target_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow_blocks", + "columnsFrom": ["target_block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_execution_logs": { + "name": "workflow_execution_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_snapshot_id": { + "name": "state_snapshot_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "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 + }, + "block_count": { + "name": "block_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "success_count": { + "name": "success_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error_count": { + "name": "error_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "skipped_count": { + "name": "skipped_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_cost": { + "name": "total_cost", + "type": "numeric(10, 6)", + "primaryKey": false, + "notNull": false + }, + "total_input_cost": { + "name": "total_input_cost", + "type": "numeric(10, 6)", + "primaryKey": false, + "notNull": false + }, + "total_output_cost": { + "name": "total_output_cost", + "type": "numeric(10, 6)", + "primaryKey": false, + "notNull": false + }, + "total_tokens": { + "name": "total_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "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_execution_id_idx": { + "name": "workflow_execution_logs_execution_id_idx", + "columns": [ + { + "expression": "execution_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": {} + } + }, + "foreignKeys": { + "workflow_execution_logs_workflow_id_workflow_id_fk": { + "name": "workflow_execution_logs_workflow_id_workflow_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_execution_logs_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" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_execution_snapshots": { + "name": "workflow_execution_snapshots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_hash": { + "name": "state_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_data": { + "name": "state_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_snapshots_workflow_id_idx": { + "name": "workflow_snapshots_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_hash_idx": { + "name": "workflow_snapshots_hash_idx", + "columns": [ + { + "expression": "state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_workflow_hash_idx": { + "name": "workflow_snapshots_workflow_hash_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_created_at_idx": { + "name": "workflow_snapshots_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_execution_snapshots_workflow_id_workflow_id_fk": { + "name": "workflow_execution_snapshots_workflow_id_workflow_id_fk", + "tableFrom": "workflow_execution_snapshots", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_folder": { + "name": "workflow_folder", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'#6B7280'" + }, + "is_expanded": { + "name": "is_expanded", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_folder_user_idx": { + "name": "workflow_folder_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_workspace_parent_idx": { + "name": "workflow_folder_workspace_parent_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_parent_sort_idx": { + "name": "workflow_folder_parent_sort_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_folder_user_id_user_id_fk": { + "name": "workflow_folder_user_id_user_id_fk", + "tableFrom": "workflow_folder", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_folder_workspace_id_workspace_id_fk": { + "name": "workflow_folder_workspace_id_workspace_id_fk", + "tableFrom": "workflow_folder", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_schedule": { + "name": "workflow_schedule", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_run_at": { + "name": "next_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_ran_at": { + "name": "last_ran_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'UTC'" + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_failed_at": { + "name": "last_failed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_schedule_workflow_block_unique": { + "name": "workflow_schedule_workflow_block_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_schedule_workflow_id_workflow_id_fk": { + "name": "workflow_schedule_workflow_id_workflow_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_block_id_workflow_blocks_id_fk": { + "name": "workflow_schedule_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow_blocks", + "columnsFrom": ["block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_subflows": { + "name": "workflow_subflows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_subflows_workflow_id_idx": { + "name": "workflow_subflows_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_subflows_workflow_type_idx": { + "name": "workflow_subflows_workflow_type_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_subflows_workflow_id_workflow_id_fk": { + "name": "workflow_subflows_workflow_id_workflow_id_fk", + "tableFrom": "workflow_subflows", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace": { + "name": "workspace", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workspace_owner_id_user_id_fk": { + "name": "workspace_owner_id_user_id_fk", + "tableFrom": "workspace", + "tableTo": "user", + "columnsFrom": ["owner_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_invitation": { + "name": "workspace_invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permissions": { + "name": "permissions", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'admin'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workspace_invitation_workspace_id_workspace_id_fk": { + "name": "workspace_invitation_workspace_id_workspace_id_fk", + "tableFrom": "workspace_invitation", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_invitation_inviter_id_user_id_fk": { + "name": "workspace_invitation_inviter_id_user_id_fk", + "tableFrom": "workspace_invitation", + "tableTo": "user", + "columnsFrom": ["inviter_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_invitation_token_unique": { + "name": "workspace_invitation_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.permission_type": { + "name": "permission_type", + "schema": "public", + "values": ["admin", "write", "read"] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/apps/sim/db/migrations/meta/0069_snapshot.json b/apps/sim/db/migrations/meta/0069_snapshot.json new file mode 100644 index 0000000000..d72651e023 --- /dev/null +++ b/apps/sim/db/migrations/meta/0069_snapshot.json @@ -0,0 +1,5863 @@ +{ + "id": "f6e062a1-6fda-4ca2-b718-d9e8b5581cb2", + "prevId": "28799b2c-5dc3-4b16-a5a1-886b68d54fd3", + "version": "7", + "dialect": "postgresql", + "tables": { + "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": {}, + "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 + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "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": {}, + "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" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "api_key_key_unique": { + "name": "api_key_key_unique", + "nullsNotDistinct": false, + "columns": ["key"] + } + }, + "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 + }, + "subdomain": { + "name": "subdomain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "customizations": { + "name": "customizations", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'public'" + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_emails": { + "name": "allowed_emails", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "output_configs": { + "name": "output_configs", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "subdomain_idx": { + "name": "subdomain_idx", + "columns": [ + { + "expression": "subdomain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_workflow_id_workflow_id_fk": { + "name": "chat_workflow_id_workflow_id_fk", + "tableFrom": "chat", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_user_id_user_id_fk": { + "name": "chat_user_id_user_id_fk", + "tableFrom": "chat", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_chats": { + "name": "copilot_chats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "messages": { + "name": "messages", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'claude-3-7-sonnet-latest'" + }, + "preview_yaml": { + "name": "preview_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_chats_user_id_idx": { + "name": "copilot_chats_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_workflow_id_idx": { + "name": "copilot_chats_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_workflow_idx": { + "name": "copilot_chats_user_workflow_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_created_at_idx": { + "name": "copilot_chats_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_updated_at_idx": { + "name": "copilot_chats_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_chats_user_id_user_id_fk": { + "name": "copilot_chats_user_id_user_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_chats_workflow_id_workflow_id_fk": { + "name": "copilot_chats_workflow_id_workflow_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_feedback": { + "name": "copilot_feedback", + "schema": "", + "columns": { + "feedback_id": { + "name": "feedback_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_query": { + "name": "user_query", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_response": { + "name": "agent_response", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_positive": { + "name": "is_positive", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "feedback": { + "name": "feedback", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_yaml": { + "name": "workflow_yaml", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_feedback_user_id_idx": { + "name": "copilot_feedback_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_chat_id_idx": { + "name": "copilot_feedback_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_user_chat_idx": { + "name": "copilot_feedback_user_chat_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_is_positive_idx": { + "name": "copilot_feedback_is_positive_idx", + "columns": [ + { + "expression": "is_positive", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_created_at_idx": { + "name": "copilot_feedback_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_feedback_user_id_user_id_fk": { + "name": "copilot_feedback_user_id_user_id_fk", + "tableFrom": "copilot_feedback", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_feedback_chat_id_copilot_chats_id_fk": { + "name": "copilot_feedback_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_feedback", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.custom_tools": { + "name": "custom_tools", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "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": {}, + "foreignKeys": { + "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": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.docs_embeddings": { + "name": "docs_embeddings", + "schema": "", + "columns": { + "chunk_id": { + "name": "chunk_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chunk_text": { + "name": "chunk_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_document": { + "name": "source_document", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_link": { + "name": "source_link", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "header_text": { + "name": "header_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "header_level": { + "name": "header_level", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": true + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "chunk_text_tsv": { + "name": "chunk_text_tsv", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "to_tsvector('english', \"docs_embeddings\".\"chunk_text\")", + "type": "stored" + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "docs_emb_source_document_idx": { + "name": "docs_emb_source_document_idx", + "columns": [ + { + "expression": "source_document", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_header_level_idx": { + "name": "docs_emb_header_level_idx", + "columns": [ + { + "expression": "header_level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_source_header_idx": { + "name": "docs_emb_source_header_idx", + "columns": [ + { + "expression": "source_document", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "header_level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_model_idx": { + "name": "docs_emb_model_idx", + "columns": [ + { + "expression": "embedding_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_created_at_idx": { + "name": "docs_emb_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_embedding_vector_hnsw_idx": { + "name": "docs_embedding_vector_hnsw_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "docs_emb_metadata_gin_idx": { + "name": "docs_emb_metadata_gin_idx", + "columns": [ + { + "expression": "metadata", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "docs_emb_chunk_text_fts_idx": { + "name": "docs_emb_chunk_text_fts_idx", + "columns": [ + { + "expression": "chunk_text_tsv", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "docs_embedding_not_null_check": { + "name": "docs_embedding_not_null_check", + "value": "\"embedding\" IS NOT NULL" + }, + "docs_header_level_check": { + "name": "docs_header_level_check", + "value": "\"header_level\" >= 1 AND \"header_level\" <= 6" + } + }, + "isRLSEnabled": false + }, + "public.document": { + "name": "document", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_url": { + "name": "file_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_size": { + "name": "file_size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_count": { + "name": "chunk_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "character_count": { + "name": "character_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "processing_status": { + "name": "processing_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "processing_started_at": { + "name": "processing_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_completed_at": { + "name": "processing_completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_error": { + "name": "processing_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "tag1": { + "name": "tag1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag2": { + "name": "tag2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag3": { + "name": "tag3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag4": { + "name": "tag4", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag5": { + "name": "tag5", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag6": { + "name": "tag6", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag7": { + "name": "tag7", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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_kb_uploaded_at_idx": { + "name": "doc_kb_uploaded_at_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "uploaded_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_processing_status_idx": { + "name": "doc_processing_status_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "processing_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag1_idx": { + "name": "doc_tag1_idx", + "columns": [ + { + "expression": "tag1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag2_idx": { + "name": "doc_tag2_idx", + "columns": [ + { + "expression": "tag2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag3_idx": { + "name": "doc_tag3_idx", + "columns": [ + { + "expression": "tag3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag4_idx": { + "name": "doc_tag4_idx", + "columns": [ + { + "expression": "tag4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag5_idx": { + "name": "doc_tag5_idx", + "columns": [ + { + "expression": "tag5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag6_idx": { + "name": "doc_tag6_idx", + "columns": [ + { + "expression": "tag6", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag7_idx": { + "name": "doc_tag7_idx", + "columns": [ + { + "expression": "tag7", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "document_knowledge_base_id_knowledge_base_id_fk": { + "name": "document_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "document", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.embedding": { + "name": "embedding", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_index": { + "name": "chunk_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "chunk_hash": { + "name": "chunk_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_length": { + "name": "content_length", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": false + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "start_offset": { + "name": "start_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "end_offset": { + "name": "end_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "tag1": { + "name": "tag1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag2": { + "name": "tag2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag3": { + "name": "tag3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag4": { + "name": "tag4", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag5": { + "name": "tag5", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag6": { + "name": "tag6", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag7": { + "name": "tag7", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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_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.invitation": { + "name": "invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "invitation_inviter_id_user_id_fk": { + "name": "invitation_inviter_id_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "columnsFrom": ["inviter_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_base": { + "name": "knowledge_base", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "embedding_dimension": { + "name": "embedding_dimension", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1536 + }, + "chunking_config": { + "name": "chunking_config", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{\"maxSize\": 1024, \"minSize\": 100, \"overlap\": 200}'" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kb_user_id_idx": { + "name": "kb_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_id_idx": { + "name": "kb_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_user_workspace_idx": { + "name": "kb_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_deleted_at_idx": { + "name": "kb_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_base_user_id_user_id_fk": { + "name": "knowledge_base_user_id_user_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_base_workspace_id_workspace_id_fk": { + "name": "knowledge_base_workspace_id_workspace_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_base_tag_definitions": { + "name": "knowledge_base_tag_definitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tag_slot": { + "name": "tag_slot", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "field_type": { + "name": "field_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kb_tag_definitions_kb_slot_idx": { + "name": "kb_tag_definitions_kb_slot_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tag_slot", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_tag_definitions_kb_display_name_idx": { + "name": "kb_tag_definitions_kb_display_name_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "display_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_tag_definitions_kb_id_idx": { + "name": "kb_tag_definitions_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk": { + "name": "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "knowledge_base_tag_definitions", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.marketplace": { + "name": "marketplace", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "author_id": { + "name": "author_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author_name": { + "name": "author_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "views": { + "name": "views", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "category": { + "name": "category", + "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": {}, + "foreignKeys": { + "marketplace_workflow_id_workflow_id_fk": { + "name": "marketplace_workflow_id_workflow_id_fk", + "tableFrom": "marketplace", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "marketplace_author_id_user_id_fk": { + "name": "marketplace_author_id_user_id_fk", + "tableFrom": "marketplace", + "tableTo": "user", + "columnsFrom": ["author_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "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": {}, + "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 + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "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()" + }, + "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_workflow_idx": { + "name": "memory_workflow_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workflow_key_idx": { + "name": "memory_workflow_key_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "memory_workflow_id_workflow_id_fk": { + "name": "memory_workflow_id_workflow_id_fk", + "tableFrom": "memory", + "tableTo": "workflow", + "columnsFrom": ["workflow_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 + }, + "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.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.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "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 + }, + "auto_fill_env_vars": { + "name": "auto_fill_env_vars", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "auto_pan": { + "name": "auto_pan", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "console_expanded_by_default": { + "name": "console_expanded_by_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "telemetry_enabled": { + "name": "telemetry_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "telemetry_notified_user": { + "name": "telemetry_notified_user", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "email_preferences": { + "name": "email_preferences", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "general": { + "name": "general", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "settings_user_id_user_id_fk": { + "name": "settings_user_id_user_id_fk", + "tableFrom": "settings", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "settings_user_id_unique": { + "name": "settings_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.subscription": { + "name": "subscription", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "plan": { + "name": "plan", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "period_start": { + "name": "period_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "period_end": { + "name": "period_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "cancel_at_period_end": { + "name": "cancel_at_period_end", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "seats": { + "name": "seats", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "trial_start": { + "name": "trial_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "trial_end": { + "name": "trial_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "subscription_reference_status_idx": { + "name": "subscription_reference_status_idx", + "columns": [ + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "check_enterprise_metadata": { + "name": "check_enterprise_metadata", + "value": "plan != 'enterprise' OR (metadata IS NOT NULL AND (metadata->>'perSeatAllowance' IS NOT NULL OR metadata->>'totalAllowance' IS NOT NULL))" + } + }, + "isRLSEnabled": false + }, + "public.template_stars": { + "name": "template_stars", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "template_id": { + "name": "template_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "starred_at": { + "name": "starred_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "template_stars_user_id_idx": { + "name": "template_stars_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_template_id_idx": { + "name": "template_stars_template_id_idx", + "columns": [ + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_user_template_idx": { + "name": "template_stars_user_template_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_template_user_idx": { + "name": "template_stars_template_user_idx", + "columns": [ + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_starred_at_idx": { + "name": "template_stars_starred_at_idx", + "columns": [ + { + "expression": "starred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_template_starred_at_idx": { + "name": "template_stars_template_starred_at_idx", + "columns": [ + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "starred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_user_template_unique": { + "name": "template_stars_user_template_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "template_stars_user_id_user_id_fk": { + "name": "template_stars_user_id_user_id_fk", + "tableFrom": "template_stars", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "template_stars_template_id_templates_id_fk": { + "name": "template_stars_template_id_templates_id_fk", + "tableFrom": "template_stars", + "tableTo": "templates", + "columnsFrom": ["template_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.templates": { + "name": "templates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_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 + }, + "author": { + "name": "author", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "views": { + "name": "views", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "stars": { + "name": "stars", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'#3972F6'" + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'FileText'" + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "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()" + } + }, + "indexes": { + "templates_workflow_id_idx": { + "name": "templates_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_user_id_idx": { + "name": "templates_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_category_idx": { + "name": "templates_category_idx", + "columns": [ + { + "expression": "category", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_views_idx": { + "name": "templates_views_idx", + "columns": [ + { + "expression": "views", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_stars_idx": { + "name": "templates_stars_idx", + "columns": [ + { + "expression": "stars", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_category_views_idx": { + "name": "templates_category_views_idx", + "columns": [ + { + "expression": "category", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "views", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_category_stars_idx": { + "name": "templates_category_stars_idx", + "columns": [ + { + "expression": "category", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "stars", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_user_category_idx": { + "name": "templates_user_category_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "category", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_created_at_idx": { + "name": "templates_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_updated_at_idx": { + "name": "templates_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "templates_workflow_id_workflow_id_fk": { + "name": "templates_workflow_id_workflow_id_fk", + "tableFrom": "templates", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "templates_user_id_user_id_fk": { + "name": "templates_user_id_user_id_fk", + "tableFrom": "templates", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_rate_limits": { + "name": "user_rate_limits", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "sync_api_requests": { + "name": "sync_api_requests", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "async_api_requests": { + "name": "async_api_requests", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "window_start": { + "name": "window_start", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_request_at": { + "name": "last_request_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "is_rate_limited": { + "name": "is_rate_limited", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "rate_limit_reset_at": { + "name": "rate_limit_reset_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_rate_limits_user_id_user_id_fk": { + "name": "user_rate_limits_user_id_user_id_fk", + "tableFrom": "user_rate_limits", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_stats": { + "name": "user_stats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "total_manual_executions": { + "name": "total_manual_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_api_calls": { + "name": "total_api_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_webhook_triggers": { + "name": "total_webhook_triggers", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_scheduled_executions": { + "name": "total_scheduled_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_chat_executions": { + "name": "total_chat_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_tokens_used": { + "name": "total_tokens_used", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_cost": { + "name": "total_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_usage_limit": { + "name": "current_usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'10'" + }, + "usage_limit_set_by": { + "name": "usage_limit_set_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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'" + }, + "billing_period_start": { + "name": "billing_period_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "billing_period_end": { + "name": "billing_period_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_period_cost": { + "name": "last_period_cost", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "last_active": { + "name": "last_active", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_stats_user_id_user_id_fk": { + "name": "user_stats_user_id_user_id_fk", + "tableFrom": "user_stats", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_stats_user_id_unique": { + "name": "user_stats_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.waitlist": { + "name": "waitlist", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "waitlist_email_unique": { + "name": "waitlist_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook": { + "name": "webhook", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_config": { + "name": "provider_config", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "path_idx": { + "name": "path_idx", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_workflow_id_workflow_id_fk": { + "name": "webhook_workflow_id_workflow_id_fk", + "tableFrom": "webhook", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webhook_block_id_workflow_blocks_id_fk": { + "name": "webhook_block_id_workflow_blocks_id_fk", + "tableFrom": "webhook", + "tableTo": "workflow_blocks", + "columnsFrom": ["block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow": { + "name": "workflow", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'#3972F6'" + }, + "last_synced": { + "name": "last_synced", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "is_deployed": { + "name": "is_deployed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deployed_state": { + "name": "deployed_state", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "deployed_at": { + "name": "deployed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "collaborators": { + "name": "collaborators", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "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": "'{}'" + }, + "is_published": { + "name": "is_published", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "marketplace_data": { + "name": "marketplace_data", + "type": "json", + "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": {} + } + }, + "foreignKeys": { + "workflow_user_id_user_id_fk": { + "name": "workflow_user_id_user_id_fk", + "tableFrom": "workflow", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_workspace_id_workspace_id_fk": { + "name": "workflow_workspace_id_workspace_id_fk", + "tableFrom": "workflow", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_folder_id_workflow_folder_id_fk": { + "name": "workflow_folder_id_workflow_folder_id_fk", + "tableFrom": "workflow", + "tableTo": "workflow_folder", + "columnsFrom": ["folder_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_blocks": { + "name": "workflow_blocks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position_x": { + "name": "position_x", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "position_y": { + "name": "position_y", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "horizontal_handles": { + "name": "horizontal_handles", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "is_wide": { + "name": "is_wide", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "advanced_mode": { + "name": "advanced_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "trigger_mode": { + "name": "trigger_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "height": { + "name": "height", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "sub_blocks": { + "name": "sub_blocks", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "outputs": { + "name": "outputs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "extent": { + "name": "extent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_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_parent_id_idx": { + "name": "workflow_blocks_parent_id_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_blocks_workflow_parent_idx": { + "name": "workflow_blocks_workflow_parent_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_blocks_workflow_type_idx": { + "name": "workflow_blocks_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_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_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_source_block_idx": { + "name": "workflow_edges_source_block_idx", + "columns": [ + { + "expression": "source_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_target_block_idx": { + "name": "workflow_edges_target_block_idx", + "columns": [ + { + "expression": "target_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_workflow_source_idx": { + "name": "workflow_edges_workflow_source_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_workflow_target_idx": { + "name": "workflow_edges_workflow_target_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_edges_workflow_id_workflow_id_fk": { + "name": "workflow_edges_workflow_id_workflow_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_edges_source_block_id_workflow_blocks_id_fk": { + "name": "workflow_edges_source_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow_blocks", + "columnsFrom": ["source_block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_edges_target_block_id_workflow_blocks_id_fk": { + "name": "workflow_edges_target_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow_blocks", + "columnsFrom": ["target_block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_execution_logs": { + "name": "workflow_execution_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_snapshot_id": { + "name": "state_snapshot_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "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 + }, + "block_count": { + "name": "block_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "success_count": { + "name": "success_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error_count": { + "name": "error_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "skipped_count": { + "name": "skipped_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_cost": { + "name": "total_cost", + "type": "numeric(10, 6)", + "primaryKey": false, + "notNull": false + }, + "total_input_cost": { + "name": "total_input_cost", + "type": "numeric(10, 6)", + "primaryKey": false, + "notNull": false + }, + "total_output_cost": { + "name": "total_output_cost", + "type": "numeric(10, 6)", + "primaryKey": false, + "notNull": false + }, + "total_tokens": { + "name": "total_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "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_execution_id_idx": { + "name": "workflow_execution_logs_execution_id_idx", + "columns": [ + { + "expression": "execution_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": {} + } + }, + "foreignKeys": { + "workflow_execution_logs_workflow_id_workflow_id_fk": { + "name": "workflow_execution_logs_workflow_id_workflow_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_execution_logs_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" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_execution_snapshots": { + "name": "workflow_execution_snapshots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_hash": { + "name": "state_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_data": { + "name": "state_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_snapshots_workflow_id_idx": { + "name": "workflow_snapshots_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_hash_idx": { + "name": "workflow_snapshots_hash_idx", + "columns": [ + { + "expression": "state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_workflow_hash_idx": { + "name": "workflow_snapshots_workflow_hash_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_created_at_idx": { + "name": "workflow_snapshots_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_execution_snapshots_workflow_id_workflow_id_fk": { + "name": "workflow_execution_snapshots_workflow_id_workflow_id_fk", + "tableFrom": "workflow_execution_snapshots", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_folder": { + "name": "workflow_folder", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'#6B7280'" + }, + "is_expanded": { + "name": "is_expanded", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_folder_user_idx": { + "name": "workflow_folder_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_workspace_parent_idx": { + "name": "workflow_folder_workspace_parent_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_parent_sort_idx": { + "name": "workflow_folder_parent_sort_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_folder_user_id_user_id_fk": { + "name": "workflow_folder_user_id_user_id_fk", + "tableFrom": "workflow_folder", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_folder_workspace_id_workspace_id_fk": { + "name": "workflow_folder_workspace_id_workspace_id_fk", + "tableFrom": "workflow_folder", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_schedule": { + "name": "workflow_schedule", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_run_at": { + "name": "next_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_ran_at": { + "name": "last_ran_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'UTC'" + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_failed_at": { + "name": "last_failed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_schedule_workflow_block_unique": { + "name": "workflow_schedule_workflow_block_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_schedule_workflow_id_workflow_id_fk": { + "name": "workflow_schedule_workflow_id_workflow_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_block_id_workflow_blocks_id_fk": { + "name": "workflow_schedule_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow_blocks", + "columnsFrom": ["block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_subflows": { + "name": "workflow_subflows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_subflows_workflow_id_idx": { + "name": "workflow_subflows_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_subflows_workflow_type_idx": { + "name": "workflow_subflows_workflow_type_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_subflows_workflow_id_workflow_id_fk": { + "name": "workflow_subflows_workflow_id_workflow_id_fk", + "tableFrom": "workflow_subflows", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace": { + "name": "workspace", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workspace_owner_id_user_id_fk": { + "name": "workspace_owner_id_user_id_fk", + "tableFrom": "workspace", + "tableTo": "user", + "columnsFrom": ["owner_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_invitation": { + "name": "workspace_invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permissions": { + "name": "permissions", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'admin'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workspace_invitation_workspace_id_workspace_id_fk": { + "name": "workspace_invitation_workspace_id_workspace_id_fk", + "tableFrom": "workspace_invitation", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_invitation_inviter_id_user_id_fk": { + "name": "workspace_invitation_inviter_id_user_id_fk", + "tableFrom": "workspace_invitation", + "tableTo": "user", + "columnsFrom": ["inviter_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_invitation_token_unique": { + "name": "workspace_invitation_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.permission_type": { + "name": "permission_type", + "schema": "public", + "values": ["admin", "write", "read"] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/apps/sim/db/migrations/meta/0070_snapshot.json b/apps/sim/db/migrations/meta/0070_snapshot.json new file mode 100644 index 0000000000..fc48f8e3f6 --- /dev/null +++ b/apps/sim/db/migrations/meta/0070_snapshot.json @@ -0,0 +1,5863 @@ +{ + "id": "347741d1-1773-4b35-8897-ca7519e06db1", + "prevId": "f6e062a1-6fda-4ca2-b718-d9e8b5581cb2", + "version": "7", + "dialect": "postgresql", + "tables": { + "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": {}, + "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 + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "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": {}, + "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" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "api_key_key_unique": { + "name": "api_key_key_unique", + "nullsNotDistinct": false, + "columns": ["key"] + } + }, + "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 + }, + "subdomain": { + "name": "subdomain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "customizations": { + "name": "customizations", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'public'" + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_emails": { + "name": "allowed_emails", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "output_configs": { + "name": "output_configs", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "subdomain_idx": { + "name": "subdomain_idx", + "columns": [ + { + "expression": "subdomain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_workflow_id_workflow_id_fk": { + "name": "chat_workflow_id_workflow_id_fk", + "tableFrom": "chat", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_user_id_user_id_fk": { + "name": "chat_user_id_user_id_fk", + "tableFrom": "chat", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_chats": { + "name": "copilot_chats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "messages": { + "name": "messages", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'claude-3-7-sonnet-latest'" + }, + "preview_yaml": { + "name": "preview_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_chats_user_id_idx": { + "name": "copilot_chats_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_workflow_id_idx": { + "name": "copilot_chats_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_workflow_idx": { + "name": "copilot_chats_user_workflow_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_created_at_idx": { + "name": "copilot_chats_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_updated_at_idx": { + "name": "copilot_chats_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_chats_user_id_user_id_fk": { + "name": "copilot_chats_user_id_user_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_chats_workflow_id_workflow_id_fk": { + "name": "copilot_chats_workflow_id_workflow_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_feedback": { + "name": "copilot_feedback", + "schema": "", + "columns": { + "feedback_id": { + "name": "feedback_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_query": { + "name": "user_query", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_response": { + "name": "agent_response", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_positive": { + "name": "is_positive", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "feedback": { + "name": "feedback", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_yaml": { + "name": "workflow_yaml", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_feedback_user_id_idx": { + "name": "copilot_feedback_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_chat_id_idx": { + "name": "copilot_feedback_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_user_chat_idx": { + "name": "copilot_feedback_user_chat_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_is_positive_idx": { + "name": "copilot_feedback_is_positive_idx", + "columns": [ + { + "expression": "is_positive", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_created_at_idx": { + "name": "copilot_feedback_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_feedback_user_id_user_id_fk": { + "name": "copilot_feedback_user_id_user_id_fk", + "tableFrom": "copilot_feedback", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_feedback_chat_id_copilot_chats_id_fk": { + "name": "copilot_feedback_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_feedback", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.custom_tools": { + "name": "custom_tools", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "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": {}, + "foreignKeys": { + "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": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.docs_embeddings": { + "name": "docs_embeddings", + "schema": "", + "columns": { + "chunk_id": { + "name": "chunk_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chunk_text": { + "name": "chunk_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_document": { + "name": "source_document", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_link": { + "name": "source_link", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "header_text": { + "name": "header_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "header_level": { + "name": "header_level", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": true + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "chunk_text_tsv": { + "name": "chunk_text_tsv", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "to_tsvector('english', \"docs_embeddings\".\"chunk_text\")", + "type": "stored" + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "docs_emb_source_document_idx": { + "name": "docs_emb_source_document_idx", + "columns": [ + { + "expression": "source_document", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_header_level_idx": { + "name": "docs_emb_header_level_idx", + "columns": [ + { + "expression": "header_level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_source_header_idx": { + "name": "docs_emb_source_header_idx", + "columns": [ + { + "expression": "source_document", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "header_level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_model_idx": { + "name": "docs_emb_model_idx", + "columns": [ + { + "expression": "embedding_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_created_at_idx": { + "name": "docs_emb_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_embedding_vector_hnsw_idx": { + "name": "docs_embedding_vector_hnsw_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "docs_emb_metadata_gin_idx": { + "name": "docs_emb_metadata_gin_idx", + "columns": [ + { + "expression": "metadata", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "docs_emb_chunk_text_fts_idx": { + "name": "docs_emb_chunk_text_fts_idx", + "columns": [ + { + "expression": "chunk_text_tsv", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "docs_embedding_not_null_check": { + "name": "docs_embedding_not_null_check", + "value": "\"embedding\" IS NOT NULL" + }, + "docs_header_level_check": { + "name": "docs_header_level_check", + "value": "\"header_level\" >= 1 AND \"header_level\" <= 6" + } + }, + "isRLSEnabled": false + }, + "public.document": { + "name": "document", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_url": { + "name": "file_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_size": { + "name": "file_size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_count": { + "name": "chunk_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "character_count": { + "name": "character_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "processing_status": { + "name": "processing_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "processing_started_at": { + "name": "processing_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_completed_at": { + "name": "processing_completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_error": { + "name": "processing_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "tag1": { + "name": "tag1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag2": { + "name": "tag2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag3": { + "name": "tag3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag4": { + "name": "tag4", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag5": { + "name": "tag5", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag6": { + "name": "tag6", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag7": { + "name": "tag7", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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_kb_uploaded_at_idx": { + "name": "doc_kb_uploaded_at_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "uploaded_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_processing_status_idx": { + "name": "doc_processing_status_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "processing_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag1_idx": { + "name": "doc_tag1_idx", + "columns": [ + { + "expression": "tag1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag2_idx": { + "name": "doc_tag2_idx", + "columns": [ + { + "expression": "tag2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag3_idx": { + "name": "doc_tag3_idx", + "columns": [ + { + "expression": "tag3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag4_idx": { + "name": "doc_tag4_idx", + "columns": [ + { + "expression": "tag4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag5_idx": { + "name": "doc_tag5_idx", + "columns": [ + { + "expression": "tag5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag6_idx": { + "name": "doc_tag6_idx", + "columns": [ + { + "expression": "tag6", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag7_idx": { + "name": "doc_tag7_idx", + "columns": [ + { + "expression": "tag7", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "document_knowledge_base_id_knowledge_base_id_fk": { + "name": "document_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "document", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.embedding": { + "name": "embedding", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_index": { + "name": "chunk_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "chunk_hash": { + "name": "chunk_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_length": { + "name": "content_length", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": false + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "start_offset": { + "name": "start_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "end_offset": { + "name": "end_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "tag1": { + "name": "tag1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag2": { + "name": "tag2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag3": { + "name": "tag3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag4": { + "name": "tag4", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag5": { + "name": "tag5", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag6": { + "name": "tag6", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag7": { + "name": "tag7", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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_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.invitation": { + "name": "invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "invitation_inviter_id_user_id_fk": { + "name": "invitation_inviter_id_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "columnsFrom": ["inviter_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_base": { + "name": "knowledge_base", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "embedding_dimension": { + "name": "embedding_dimension", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1536 + }, + "chunking_config": { + "name": "chunking_config", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{\"maxSize\": 1024, \"minSize\": 1, \"overlap\": 200}'" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kb_user_id_idx": { + "name": "kb_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_id_idx": { + "name": "kb_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_user_workspace_idx": { + "name": "kb_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_deleted_at_idx": { + "name": "kb_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_base_user_id_user_id_fk": { + "name": "knowledge_base_user_id_user_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_base_workspace_id_workspace_id_fk": { + "name": "knowledge_base_workspace_id_workspace_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_base_tag_definitions": { + "name": "knowledge_base_tag_definitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tag_slot": { + "name": "tag_slot", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "field_type": { + "name": "field_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kb_tag_definitions_kb_slot_idx": { + "name": "kb_tag_definitions_kb_slot_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tag_slot", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_tag_definitions_kb_display_name_idx": { + "name": "kb_tag_definitions_kb_display_name_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "display_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_tag_definitions_kb_id_idx": { + "name": "kb_tag_definitions_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk": { + "name": "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "knowledge_base_tag_definitions", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.marketplace": { + "name": "marketplace", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "author_id": { + "name": "author_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author_name": { + "name": "author_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "views": { + "name": "views", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "category": { + "name": "category", + "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": {}, + "foreignKeys": { + "marketplace_workflow_id_workflow_id_fk": { + "name": "marketplace_workflow_id_workflow_id_fk", + "tableFrom": "marketplace", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "marketplace_author_id_user_id_fk": { + "name": "marketplace_author_id_user_id_fk", + "tableFrom": "marketplace", + "tableTo": "user", + "columnsFrom": ["author_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "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": {}, + "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 + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "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()" + }, + "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_workflow_idx": { + "name": "memory_workflow_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workflow_key_idx": { + "name": "memory_workflow_key_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "memory_workflow_id_workflow_id_fk": { + "name": "memory_workflow_id_workflow_id_fk", + "tableFrom": "memory", + "tableTo": "workflow", + "columnsFrom": ["workflow_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 + }, + "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.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.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "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 + }, + "auto_fill_env_vars": { + "name": "auto_fill_env_vars", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "auto_pan": { + "name": "auto_pan", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "console_expanded_by_default": { + "name": "console_expanded_by_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "telemetry_enabled": { + "name": "telemetry_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "telemetry_notified_user": { + "name": "telemetry_notified_user", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "email_preferences": { + "name": "email_preferences", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "general": { + "name": "general", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "settings_user_id_user_id_fk": { + "name": "settings_user_id_user_id_fk", + "tableFrom": "settings", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "settings_user_id_unique": { + "name": "settings_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.subscription": { + "name": "subscription", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "plan": { + "name": "plan", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "period_start": { + "name": "period_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "period_end": { + "name": "period_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "cancel_at_period_end": { + "name": "cancel_at_period_end", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "seats": { + "name": "seats", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "trial_start": { + "name": "trial_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "trial_end": { + "name": "trial_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "subscription_reference_status_idx": { + "name": "subscription_reference_status_idx", + "columns": [ + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "check_enterprise_metadata": { + "name": "check_enterprise_metadata", + "value": "plan != 'enterprise' OR (metadata IS NOT NULL AND (metadata->>'perSeatAllowance' IS NOT NULL OR metadata->>'totalAllowance' IS NOT NULL))" + } + }, + "isRLSEnabled": false + }, + "public.template_stars": { + "name": "template_stars", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "template_id": { + "name": "template_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "starred_at": { + "name": "starred_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "template_stars_user_id_idx": { + "name": "template_stars_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_template_id_idx": { + "name": "template_stars_template_id_idx", + "columns": [ + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_user_template_idx": { + "name": "template_stars_user_template_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_template_user_idx": { + "name": "template_stars_template_user_idx", + "columns": [ + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_starred_at_idx": { + "name": "template_stars_starred_at_idx", + "columns": [ + { + "expression": "starred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_template_starred_at_idx": { + "name": "template_stars_template_starred_at_idx", + "columns": [ + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "starred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_user_template_unique": { + "name": "template_stars_user_template_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "template_stars_user_id_user_id_fk": { + "name": "template_stars_user_id_user_id_fk", + "tableFrom": "template_stars", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "template_stars_template_id_templates_id_fk": { + "name": "template_stars_template_id_templates_id_fk", + "tableFrom": "template_stars", + "tableTo": "templates", + "columnsFrom": ["template_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.templates": { + "name": "templates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_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 + }, + "author": { + "name": "author", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "views": { + "name": "views", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "stars": { + "name": "stars", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'#3972F6'" + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'FileText'" + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "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()" + } + }, + "indexes": { + "templates_workflow_id_idx": { + "name": "templates_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_user_id_idx": { + "name": "templates_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_category_idx": { + "name": "templates_category_idx", + "columns": [ + { + "expression": "category", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_views_idx": { + "name": "templates_views_idx", + "columns": [ + { + "expression": "views", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_stars_idx": { + "name": "templates_stars_idx", + "columns": [ + { + "expression": "stars", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_category_views_idx": { + "name": "templates_category_views_idx", + "columns": [ + { + "expression": "category", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "views", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_category_stars_idx": { + "name": "templates_category_stars_idx", + "columns": [ + { + "expression": "category", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "stars", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_user_category_idx": { + "name": "templates_user_category_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "category", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_created_at_idx": { + "name": "templates_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_updated_at_idx": { + "name": "templates_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "templates_workflow_id_workflow_id_fk": { + "name": "templates_workflow_id_workflow_id_fk", + "tableFrom": "templates", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "templates_user_id_user_id_fk": { + "name": "templates_user_id_user_id_fk", + "tableFrom": "templates", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_rate_limits": { + "name": "user_rate_limits", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "sync_api_requests": { + "name": "sync_api_requests", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "async_api_requests": { + "name": "async_api_requests", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "window_start": { + "name": "window_start", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_request_at": { + "name": "last_request_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "is_rate_limited": { + "name": "is_rate_limited", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "rate_limit_reset_at": { + "name": "rate_limit_reset_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_rate_limits_user_id_user_id_fk": { + "name": "user_rate_limits_user_id_user_id_fk", + "tableFrom": "user_rate_limits", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_stats": { + "name": "user_stats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "total_manual_executions": { + "name": "total_manual_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_api_calls": { + "name": "total_api_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_webhook_triggers": { + "name": "total_webhook_triggers", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_scheduled_executions": { + "name": "total_scheduled_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_chat_executions": { + "name": "total_chat_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_tokens_used": { + "name": "total_tokens_used", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_cost": { + "name": "total_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_usage_limit": { + "name": "current_usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'10'" + }, + "usage_limit_set_by": { + "name": "usage_limit_set_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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'" + }, + "billing_period_start": { + "name": "billing_period_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "billing_period_end": { + "name": "billing_period_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_period_cost": { + "name": "last_period_cost", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "last_active": { + "name": "last_active", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_stats_user_id_user_id_fk": { + "name": "user_stats_user_id_user_id_fk", + "tableFrom": "user_stats", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_stats_user_id_unique": { + "name": "user_stats_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.waitlist": { + "name": "waitlist", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "waitlist_email_unique": { + "name": "waitlist_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook": { + "name": "webhook", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_config": { + "name": "provider_config", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "path_idx": { + "name": "path_idx", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_workflow_id_workflow_id_fk": { + "name": "webhook_workflow_id_workflow_id_fk", + "tableFrom": "webhook", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webhook_block_id_workflow_blocks_id_fk": { + "name": "webhook_block_id_workflow_blocks_id_fk", + "tableFrom": "webhook", + "tableTo": "workflow_blocks", + "columnsFrom": ["block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow": { + "name": "workflow", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'#3972F6'" + }, + "last_synced": { + "name": "last_synced", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "is_deployed": { + "name": "is_deployed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deployed_state": { + "name": "deployed_state", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "deployed_at": { + "name": "deployed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "collaborators": { + "name": "collaborators", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "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": "'{}'" + }, + "is_published": { + "name": "is_published", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "marketplace_data": { + "name": "marketplace_data", + "type": "json", + "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": {} + } + }, + "foreignKeys": { + "workflow_user_id_user_id_fk": { + "name": "workflow_user_id_user_id_fk", + "tableFrom": "workflow", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_workspace_id_workspace_id_fk": { + "name": "workflow_workspace_id_workspace_id_fk", + "tableFrom": "workflow", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_folder_id_workflow_folder_id_fk": { + "name": "workflow_folder_id_workflow_folder_id_fk", + "tableFrom": "workflow", + "tableTo": "workflow_folder", + "columnsFrom": ["folder_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_blocks": { + "name": "workflow_blocks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position_x": { + "name": "position_x", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "position_y": { + "name": "position_y", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "horizontal_handles": { + "name": "horizontal_handles", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "is_wide": { + "name": "is_wide", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "advanced_mode": { + "name": "advanced_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "trigger_mode": { + "name": "trigger_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "height": { + "name": "height", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "sub_blocks": { + "name": "sub_blocks", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "outputs": { + "name": "outputs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "extent": { + "name": "extent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_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_parent_id_idx": { + "name": "workflow_blocks_parent_id_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_blocks_workflow_parent_idx": { + "name": "workflow_blocks_workflow_parent_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_blocks_workflow_type_idx": { + "name": "workflow_blocks_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_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_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_source_block_idx": { + "name": "workflow_edges_source_block_idx", + "columns": [ + { + "expression": "source_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_target_block_idx": { + "name": "workflow_edges_target_block_idx", + "columns": [ + { + "expression": "target_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_workflow_source_idx": { + "name": "workflow_edges_workflow_source_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_workflow_target_idx": { + "name": "workflow_edges_workflow_target_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_edges_workflow_id_workflow_id_fk": { + "name": "workflow_edges_workflow_id_workflow_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_edges_source_block_id_workflow_blocks_id_fk": { + "name": "workflow_edges_source_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow_blocks", + "columnsFrom": ["source_block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_edges_target_block_id_workflow_blocks_id_fk": { + "name": "workflow_edges_target_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow_blocks", + "columnsFrom": ["target_block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_execution_logs": { + "name": "workflow_execution_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_snapshot_id": { + "name": "state_snapshot_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "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 + }, + "block_count": { + "name": "block_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "success_count": { + "name": "success_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error_count": { + "name": "error_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "skipped_count": { + "name": "skipped_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_cost": { + "name": "total_cost", + "type": "numeric(10, 6)", + "primaryKey": false, + "notNull": false + }, + "total_input_cost": { + "name": "total_input_cost", + "type": "numeric(10, 6)", + "primaryKey": false, + "notNull": false + }, + "total_output_cost": { + "name": "total_output_cost", + "type": "numeric(10, 6)", + "primaryKey": false, + "notNull": false + }, + "total_tokens": { + "name": "total_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "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_execution_id_idx": { + "name": "workflow_execution_logs_execution_id_idx", + "columns": [ + { + "expression": "execution_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": {} + } + }, + "foreignKeys": { + "workflow_execution_logs_workflow_id_workflow_id_fk": { + "name": "workflow_execution_logs_workflow_id_workflow_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_execution_logs_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" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_execution_snapshots": { + "name": "workflow_execution_snapshots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_hash": { + "name": "state_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_data": { + "name": "state_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_snapshots_workflow_id_idx": { + "name": "workflow_snapshots_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_hash_idx": { + "name": "workflow_snapshots_hash_idx", + "columns": [ + { + "expression": "state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_workflow_hash_idx": { + "name": "workflow_snapshots_workflow_hash_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_created_at_idx": { + "name": "workflow_snapshots_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_execution_snapshots_workflow_id_workflow_id_fk": { + "name": "workflow_execution_snapshots_workflow_id_workflow_id_fk", + "tableFrom": "workflow_execution_snapshots", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_folder": { + "name": "workflow_folder", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'#6B7280'" + }, + "is_expanded": { + "name": "is_expanded", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_folder_user_idx": { + "name": "workflow_folder_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_workspace_parent_idx": { + "name": "workflow_folder_workspace_parent_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_parent_sort_idx": { + "name": "workflow_folder_parent_sort_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_folder_user_id_user_id_fk": { + "name": "workflow_folder_user_id_user_id_fk", + "tableFrom": "workflow_folder", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_folder_workspace_id_workspace_id_fk": { + "name": "workflow_folder_workspace_id_workspace_id_fk", + "tableFrom": "workflow_folder", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_schedule": { + "name": "workflow_schedule", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_run_at": { + "name": "next_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_ran_at": { + "name": "last_ran_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'UTC'" + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_failed_at": { + "name": "last_failed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_schedule_workflow_block_unique": { + "name": "workflow_schedule_workflow_block_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_schedule_workflow_id_workflow_id_fk": { + "name": "workflow_schedule_workflow_id_workflow_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_block_id_workflow_blocks_id_fk": { + "name": "workflow_schedule_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow_blocks", + "columnsFrom": ["block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_subflows": { + "name": "workflow_subflows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_subflows_workflow_id_idx": { + "name": "workflow_subflows_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_subflows_workflow_type_idx": { + "name": "workflow_subflows_workflow_type_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_subflows_workflow_id_workflow_id_fk": { + "name": "workflow_subflows_workflow_id_workflow_id_fk", + "tableFrom": "workflow_subflows", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace": { + "name": "workspace", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workspace_owner_id_user_id_fk": { + "name": "workspace_owner_id_user_id_fk", + "tableFrom": "workspace", + "tableTo": "user", + "columnsFrom": ["owner_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_invitation": { + "name": "workspace_invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permissions": { + "name": "permissions", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'admin'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workspace_invitation_workspace_id_workspace_id_fk": { + "name": "workspace_invitation_workspace_id_workspace_id_fk", + "tableFrom": "workspace_invitation", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_invitation_inviter_id_user_id_fk": { + "name": "workspace_invitation_inviter_id_user_id_fk", + "tableFrom": "workspace_invitation", + "tableTo": "user", + "columnsFrom": ["inviter_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_invitation_token_unique": { + "name": "workspace_invitation_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.permission_type": { + "name": "permission_type", + "schema": "public", + "values": ["admin", "write", "read"] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/apps/sim/db/migrations/meta/_journal.json b/apps/sim/db/migrations/meta/_journal.json index bc7bf08ed7..3f67efa2b6 100644 --- a/apps/sim/db/migrations/meta/_journal.json +++ b/apps/sim/db/migrations/meta/_journal.json @@ -470,6 +470,27 @@ "when": 1754424644234, "tag": "0067_safe_bushwacker", "breakpoints": true + }, + { + "idx": 68, + "version": "7", + "when": 1754446277077, + "tag": "0068_fine_hardball", + "breakpoints": true + }, + { + "idx": 69, + "version": "7", + "when": 1754603754177, + "tag": "0069_lonely_spirit", + "breakpoints": true + }, + { + "idx": 70, + "version": "7", + "when": 1754682155062, + "tag": "0070_charming_wrecking_crew", + "breakpoints": true } ] } diff --git a/apps/sim/db/migrations/relations.ts b/apps/sim/db/migrations/relations.ts deleted file mode 100644 index 635c54146c..0000000000 --- a/apps/sim/db/migrations/relations.ts +++ /dev/null @@ -1,431 +0,0 @@ -import { relations } from 'drizzle-orm/relations' -import { - account, - apiKey, - chat, - copilotChats, - copilotCheckpoints, - customTools, - document, - embedding, - environment, - invitation, - knowledgeBase, - marketplace, - member, - memory, - organization, - permissions, - session, - settings, - templateStars, - templates, - user, - userRateLimits, - userStats, - webhook, - workflow, - workflowBlocks, - workflowEdges, - workflowExecutionBlocks, - workflowExecutionLogs, - workflowExecutionSnapshots, - workflowFolder, - workflowLogs, - workflowSchedule, - workflowSubflows, - workspace, - workspaceInvitation, -} from './schema' - -export const accountRelations = relations(account, ({ one }) => ({ - user: one(user, { - fields: [account.userId], - references: [user.id], - }), -})) - -export const userRelations = relations(user, ({ many }) => ({ - accounts: many(account), - environments: many(environment), - apiKeys: many(apiKey), - marketplaces: many(marketplace), - customTools: many(customTools), - sessions: many(session), - invitations: many(invitation), - members: many(member), - chats: many(chat), - workspaces: many(workspace), - knowledgeBases: many(knowledgeBase), - workflows: many(workflow), - workflowFolders: many(workflowFolder), - workspaceInvitations: many(workspaceInvitation), - permissions: many(permissions), - userStats: many(userStats), - copilotChats: many(copilotChats), - templateStars: many(templateStars), - templates: many(templates), - settings: many(settings), - userRateLimits: many(userRateLimits), - copilotCheckpoints: many(copilotCheckpoints), -})) - -export const environmentRelations = relations(environment, ({ one }) => ({ - user: one(user, { - fields: [environment.userId], - references: [user.id], - }), -})) - -export const workflowLogsRelations = relations(workflowLogs, ({ one }) => ({ - workflow: one(workflow, { - fields: [workflowLogs.workflowId], - references: [workflow.id], - }), -})) - -export const workflowRelations = relations(workflow, ({ one, many }) => ({ - workflowLogs: many(workflowLogs), - marketplaces: many(marketplace), - chats: many(chat), - memories: many(memory), - user: one(user, { - fields: [workflow.userId], - references: [user.id], - }), - workspace: one(workspace, { - fields: [workflow.workspaceId], - references: [workspace.id], - }), - workflowFolder: one(workflowFolder, { - fields: [workflow.folderId], - references: [workflowFolder.id], - }), - workflowEdges: many(workflowEdges), - workflowSubflows: many(workflowSubflows), - workflowBlocks: many(workflowBlocks), - workflowExecutionBlocks: many(workflowExecutionBlocks), - workflowExecutionLogs: many(workflowExecutionLogs), - workflowExecutionSnapshots: many(workflowExecutionSnapshots), - copilotChats: many(copilotChats), - templates: many(templates), - webhooks: many(webhook), - workflowSchedules: many(workflowSchedule), - copilotCheckpoints: many(copilotCheckpoints), -})) - -export const apiKeyRelations = relations(apiKey, ({ one }) => ({ - user: one(user, { - fields: [apiKey.userId], - references: [user.id], - }), -})) - -export const marketplaceRelations = relations(marketplace, ({ one }) => ({ - workflow: one(workflow, { - fields: [marketplace.workflowId], - references: [workflow.id], - }), - user: one(user, { - fields: [marketplace.authorId], - references: [user.id], - }), -})) - -export const customToolsRelations = relations(customTools, ({ one }) => ({ - user: one(user, { - fields: [customTools.userId], - references: [user.id], - }), -})) - -export const sessionRelations = relations(session, ({ one }) => ({ - user: one(user, { - fields: [session.userId], - references: [user.id], - }), - organization: one(organization, { - fields: [session.activeOrganizationId], - references: [organization.id], - }), -})) - -export const organizationRelations = relations(organization, ({ many }) => ({ - sessions: many(session), - invitations: many(invitation), - members: many(member), -})) - -export const invitationRelations = relations(invitation, ({ one }) => ({ - user: one(user, { - fields: [invitation.inviterId], - references: [user.id], - }), - organization: one(organization, { - fields: [invitation.organizationId], - references: [organization.id], - }), -})) - -export const memberRelations = relations(member, ({ one }) => ({ - user: one(user, { - fields: [member.userId], - references: [user.id], - }), - organization: one(organization, { - fields: [member.organizationId], - references: [organization.id], - }), -})) - -export const chatRelations = relations(chat, ({ one }) => ({ - workflow: one(workflow, { - fields: [chat.workflowId], - references: [workflow.id], - }), - user: one(user, { - fields: [chat.userId], - references: [user.id], - }), -})) - -export const workspaceRelations = relations(workspace, ({ one, many }) => ({ - user: one(user, { - fields: [workspace.ownerId], - references: [user.id], - }), - knowledgeBases: many(knowledgeBase), - workflows: many(workflow), - workflowFolders: many(workflowFolder), - workspaceInvitations: many(workspaceInvitation), -})) - -export const memoryRelations = relations(memory, ({ one }) => ({ - workflow: one(workflow, { - fields: [memory.workflowId], - references: [workflow.id], - }), -})) - -export const knowledgeBaseRelations = relations(knowledgeBase, ({ one, many }) => ({ - user: one(user, { - fields: [knowledgeBase.userId], - references: [user.id], - }), - workspace: one(workspace, { - fields: [knowledgeBase.workspaceId], - references: [workspace.id], - }), - documents: many(document), - embeddings: many(embedding), -})) - -export const workflowFolderRelations = relations(workflowFolder, ({ one, many }) => ({ - workflows: many(workflow), - user: one(user, { - fields: [workflowFolder.userId], - references: [user.id], - }), - workspace: one(workspace, { - fields: [workflowFolder.workspaceId], - references: [workspace.id], - }), -})) - -export const workflowEdgesRelations = relations(workflowEdges, ({ one }) => ({ - workflow: one(workflow, { - fields: [workflowEdges.workflowId], - references: [workflow.id], - }), - workflowBlock_sourceBlockId: one(workflowBlocks, { - fields: [workflowEdges.sourceBlockId], - references: [workflowBlocks.id], - relationName: 'workflowEdges_sourceBlockId_workflowBlocks_id', - }), - workflowBlock_targetBlockId: one(workflowBlocks, { - fields: [workflowEdges.targetBlockId], - references: [workflowBlocks.id], - relationName: 'workflowEdges_targetBlockId_workflowBlocks_id', - }), -})) - -export const workflowBlocksRelations = relations(workflowBlocks, ({ one, many }) => ({ - workflowEdges_sourceBlockId: many(workflowEdges, { - relationName: 'workflowEdges_sourceBlockId_workflowBlocks_id', - }), - workflowEdges_targetBlockId: many(workflowEdges, { - relationName: 'workflowEdges_targetBlockId_workflowBlocks_id', - }), - workflow: one(workflow, { - fields: [workflowBlocks.workflowId], - references: [workflow.id], - }), - webhooks: many(webhook), - workflowSchedules: many(workflowSchedule), -})) - -export const workflowSubflowsRelations = relations(workflowSubflows, ({ one }) => ({ - workflow: one(workflow, { - fields: [workflowSubflows.workflowId], - references: [workflow.id], - }), -})) - -export const workspaceInvitationRelations = relations(workspaceInvitation, ({ one }) => ({ - workspace: one(workspace, { - fields: [workspaceInvitation.workspaceId], - references: [workspace.id], - }), - user: one(user, { - fields: [workspaceInvitation.inviterId], - references: [user.id], - }), -})) - -export const permissionsRelations = relations(permissions, ({ one }) => ({ - user: one(user, { - fields: [permissions.userId], - references: [user.id], - }), -})) - -export const userStatsRelations = relations(userStats, ({ one }) => ({ - user: one(user, { - fields: [userStats.userId], - references: [user.id], - }), -})) - -export const workflowExecutionBlocksRelations = relations(workflowExecutionBlocks, ({ one }) => ({ - workflow: one(workflow, { - fields: [workflowExecutionBlocks.workflowId], - references: [workflow.id], - }), -})) - -export const workflowExecutionLogsRelations = relations(workflowExecutionLogs, ({ one }) => ({ - workflow: one(workflow, { - fields: [workflowExecutionLogs.workflowId], - references: [workflow.id], - }), - workflowExecutionSnapshot: one(workflowExecutionSnapshots, { - fields: [workflowExecutionLogs.stateSnapshotId], - references: [workflowExecutionSnapshots.id], - }), -})) - -export const workflowExecutionSnapshotsRelations = relations( - workflowExecutionSnapshots, - ({ one, many }) => ({ - workflowExecutionLogs: many(workflowExecutionLogs), - workflow: one(workflow, { - fields: [workflowExecutionSnapshots.workflowId], - references: [workflow.id], - }), - }) -) - -export const copilotChatsRelations = relations(copilotChats, ({ one, many }) => ({ - user: one(user, { - fields: [copilotChats.userId], - references: [user.id], - }), - workflow: one(workflow, { - fields: [copilotChats.workflowId], - references: [workflow.id], - }), - copilotCheckpoints: many(copilotCheckpoints), -})) - -export const documentRelations = relations(document, ({ one, many }) => ({ - knowledgeBase: one(knowledgeBase, { - fields: [document.knowledgeBaseId], - references: [knowledgeBase.id], - }), - embeddings: many(embedding), -})) - -export const embeddingRelations = relations(embedding, ({ one }) => ({ - knowledgeBase: one(knowledgeBase, { - fields: [embedding.knowledgeBaseId], - references: [knowledgeBase.id], - }), - document: one(document, { - fields: [embedding.documentId], - references: [document.id], - }), -})) - -export const templateStarsRelations = relations(templateStars, ({ one }) => ({ - user: one(user, { - fields: [templateStars.userId], - references: [user.id], - }), - template: one(templates, { - fields: [templateStars.templateId], - references: [templates.id], - }), -})) - -export const templatesRelations = relations(templates, ({ one, many }) => ({ - templateStars: many(templateStars), - workflow: one(workflow, { - fields: [templates.workflowId], - references: [workflow.id], - }), - user: one(user, { - fields: [templates.userId], - references: [user.id], - }), -})) - -export const settingsRelations = relations(settings, ({ one }) => ({ - user: one(user, { - fields: [settings.userId], - references: [user.id], - }), -})) - -export const userRateLimitsRelations = relations(userRateLimits, ({ one }) => ({ - user: one(user, { - fields: [userRateLimits.userId], - references: [user.id], - }), -})) - -export const webhookRelations = relations(webhook, ({ one }) => ({ - workflow: one(workflow, { - fields: [webhook.workflowId], - references: [workflow.id], - }), - workflowBlock: one(workflowBlocks, { - fields: [webhook.blockId], - references: [workflowBlocks.id], - }), -})) - -export const workflowScheduleRelations = relations(workflowSchedule, ({ one }) => ({ - workflow: one(workflow, { - fields: [workflowSchedule.workflowId], - references: [workflow.id], - }), - workflowBlock: one(workflowBlocks, { - fields: [workflowSchedule.blockId], - references: [workflowBlocks.id], - }), -})) - -export const copilotCheckpointsRelations = relations(copilotCheckpoints, ({ one }) => ({ - user: one(user, { - fields: [copilotCheckpoints.userId], - references: [user.id], - }), - workflow: one(workflow, { - fields: [copilotCheckpoints.workflowId], - references: [workflow.id], - }), - copilotChat: one(copilotChats, { - fields: [copilotCheckpoints.chatId], - references: [copilotChats.id], - }), -})) diff --git a/apps/sim/db/migrations/schema.ts b/apps/sim/db/migrations/schema.ts deleted file mode 100644 index 5d1283c66b..0000000000 --- a/apps/sim/db/migrations/schema.ts +++ /dev/null @@ -1,1472 +0,0 @@ -import { sql } from 'drizzle-orm' -import { - boolean, - check, - customType, - foreignKey, - index, - integer, - json, - jsonb, - numeric, - pgEnum, - pgTable, - text, - timestamp, - unique, - uniqueIndex, - uuid, - vector, -} from 'drizzle-orm/pg-core' - -// Custom type for PostgreSQL tsvector -const tsvector = customType<{ data: string }>({ - dataType() { - return 'tsvector' - }, -}) - -export const permissionType = pgEnum('permission_type', ['admin', 'write', 'read']) - -export const verification = pgTable('verification', { - id: text().primaryKey().notNull(), - identifier: text().notNull(), - value: text().notNull(), - expiresAt: timestamp('expires_at', { mode: 'string' }).notNull(), - createdAt: timestamp('created_at', { mode: 'string' }), - updatedAt: timestamp('updated_at', { mode: 'string' }), -}) - -export const account = pgTable( - 'account', - { - id: text().primaryKey().notNull(), - accountId: text('account_id').notNull(), - providerId: text('provider_id').notNull(), - userId: text('user_id').notNull(), - accessToken: text('access_token'), - refreshToken: text('refresh_token'), - idToken: text('id_token'), - accessTokenExpiresAt: timestamp('access_token_expires_at', { mode: 'string' }), - refreshTokenExpiresAt: timestamp('refresh_token_expires_at', { mode: 'string' }), - scope: text(), - password: text(), - createdAt: timestamp('created_at', { mode: 'string' }).notNull(), - updatedAt: timestamp('updated_at', { mode: 'string' }).notNull(), - }, - (table) => [ - foreignKey({ - columns: [table.userId], - foreignColumns: [user.id], - name: 'account_user_id_user_id_fk', - }).onDelete('cascade'), - ] -) - -export const waitlist = pgTable( - 'waitlist', - { - id: text().primaryKey().notNull(), - email: text().notNull(), - status: text().default('pending').notNull(), - createdAt: timestamp('created_at', { mode: 'string' }).defaultNow().notNull(), - updatedAt: timestamp('updated_at', { mode: 'string' }).defaultNow().notNull(), - }, - (table) => [unique('waitlist_email_unique').on(table.email)] -) - -export const environment = pgTable( - 'environment', - { - id: text().primaryKey().notNull(), - userId: text('user_id').notNull(), - variables: json().notNull(), - updatedAt: timestamp('updated_at', { mode: 'string' }).defaultNow().notNull(), - }, - (table) => [ - foreignKey({ - columns: [table.userId], - foreignColumns: [user.id], - name: 'environment_user_id_user_id_fk', - }).onDelete('cascade'), - unique('environment_user_id_unique').on(table.userId), - ] -) - -export const workflowLogs = pgTable( - 'workflow_logs', - { - id: text().primaryKey().notNull(), - workflowId: text('workflow_id').notNull(), - executionId: text('execution_id'), - level: text().notNull(), - message: text().notNull(), - createdAt: timestamp('created_at', { mode: 'string' }).defaultNow().notNull(), - duration: text(), - trigger: text(), - metadata: json(), - }, - (table) => [ - foreignKey({ - columns: [table.workflowId], - foreignColumns: [workflow.id], - name: 'workflow_logs_workflow_id_workflow_id_fk', - }).onDelete('cascade'), - ] -) - -export const apiKey = pgTable( - 'api_key', - { - id: text().primaryKey().notNull(), - userId: text('user_id').notNull(), - name: text().notNull(), - key: text().notNull(), - lastUsed: timestamp('last_used', { mode: 'string' }), - createdAt: timestamp('created_at', { mode: 'string' }).defaultNow().notNull(), - updatedAt: timestamp('updated_at', { mode: 'string' }).defaultNow().notNull(), - expiresAt: timestamp('expires_at', { mode: 'string' }), - }, - (table) => [ - foreignKey({ - columns: [table.userId], - foreignColumns: [user.id], - name: 'api_key_user_id_user_id_fk', - }).onDelete('cascade'), - unique('api_key_key_unique').on(table.key), - ] -) - -export const marketplace = pgTable( - 'marketplace', - { - id: text().primaryKey().notNull(), - workflowId: text('workflow_id').notNull(), - state: json().notNull(), - name: text().notNull(), - description: text(), - authorId: text('author_id').notNull(), - authorName: text('author_name').notNull(), - views: integer().default(0).notNull(), - category: text(), - createdAt: timestamp('created_at', { mode: 'string' }).defaultNow().notNull(), - updatedAt: timestamp('updated_at', { mode: 'string' }).defaultNow().notNull(), - }, - (table) => [ - foreignKey({ - columns: [table.workflowId], - foreignColumns: [workflow.id], - name: 'marketplace_workflow_id_workflow_id_fk', - }).onDelete('cascade'), - foreignKey({ - columns: [table.authorId], - foreignColumns: [user.id], - name: 'marketplace_author_id_user_id_fk', - }), - ] -) - -export const customTools = pgTable( - 'custom_tools', - { - id: text().primaryKey().notNull(), - userId: text('user_id').notNull(), - title: text().notNull(), - schema: json().notNull(), - code: text().notNull(), - createdAt: timestamp('created_at', { mode: 'string' }).defaultNow().notNull(), - updatedAt: timestamp('updated_at', { mode: 'string' }).defaultNow().notNull(), - }, - (table) => [ - foreignKey({ - columns: [table.userId], - foreignColumns: [user.id], - name: 'custom_tools_user_id_user_id_fk', - }).onDelete('cascade'), - ] -) - -export const user = pgTable( - 'user', - { - id: text().primaryKey().notNull(), - name: text().notNull(), - email: text().notNull(), - emailVerified: boolean('email_verified').notNull(), - image: text(), - createdAt: timestamp('created_at', { mode: 'string' }).notNull(), - updatedAt: timestamp('updated_at', { mode: 'string' }).notNull(), - stripeCustomerId: text('stripe_customer_id'), - }, - (table) => [unique('user_email_unique').on(table.email)] -) - -export const session = pgTable( - 'session', - { - id: text().primaryKey().notNull(), - expiresAt: timestamp('expires_at', { mode: 'string' }).notNull(), - token: text().notNull(), - createdAt: timestamp('created_at', { mode: 'string' }).notNull(), - updatedAt: timestamp('updated_at', { mode: 'string' }).notNull(), - ipAddress: text('ip_address'), - userAgent: text('user_agent'), - userId: text('user_id').notNull(), - activeOrganizationId: text('active_organization_id'), - }, - (table) => [ - foreignKey({ - columns: [table.userId], - foreignColumns: [user.id], - name: 'session_user_id_user_id_fk', - }).onDelete('cascade'), - foreignKey({ - columns: [table.activeOrganizationId], - foreignColumns: [organization.id], - name: 'session_active_organization_id_organization_id_fk', - }).onDelete('set null'), - unique('session_token_unique').on(table.token), - ] -) - -export const invitation = pgTable( - 'invitation', - { - id: text().primaryKey().notNull(), - email: text().notNull(), - inviterId: text('inviter_id').notNull(), - organizationId: text('organization_id').notNull(), - role: text().notNull(), - status: text().notNull(), - expiresAt: timestamp('expires_at', { mode: 'string' }).notNull(), - createdAt: timestamp('created_at', { mode: 'string' }).defaultNow().notNull(), - }, - (table) => [ - foreignKey({ - columns: [table.inviterId], - foreignColumns: [user.id], - name: 'invitation_inviter_id_user_id_fk', - }).onDelete('cascade'), - foreignKey({ - columns: [table.organizationId], - foreignColumns: [organization.id], - name: 'invitation_organization_id_organization_id_fk', - }).onDelete('cascade'), - ] -) - -export const member = pgTable( - 'member', - { - id: text().primaryKey().notNull(), - userId: text('user_id').notNull(), - organizationId: text('organization_id').notNull(), - role: text().notNull(), - createdAt: timestamp('created_at', { mode: 'string' }).defaultNow().notNull(), - }, - (table) => [ - foreignKey({ - columns: [table.userId], - foreignColumns: [user.id], - name: 'member_user_id_user_id_fk', - }).onDelete('cascade'), - foreignKey({ - columns: [table.organizationId], - foreignColumns: [organization.id], - name: 'member_organization_id_organization_id_fk', - }).onDelete('cascade'), - ] -) - -export const chat = pgTable( - 'chat', - { - id: text().primaryKey().notNull(), - workflowId: text('workflow_id').notNull(), - userId: text('user_id').notNull(), - subdomain: text().notNull(), - title: text().notNull(), - description: text(), - isActive: boolean('is_active').default(true).notNull(), - customizations: json().default({}), - authType: text('auth_type').default('public').notNull(), - password: text(), - allowedEmails: json('allowed_emails').default([]), - createdAt: timestamp('created_at', { mode: 'string' }).defaultNow().notNull(), - updatedAt: timestamp('updated_at', { mode: 'string' }).defaultNow().notNull(), - outputConfigs: json('output_configs').default([]), - }, - (table) => [ - uniqueIndex('subdomain_idx').using('btree', table.subdomain.asc().nullsLast().op('text_ops')), - foreignKey({ - columns: [table.workflowId], - foreignColumns: [workflow.id], - name: 'chat_workflow_id_workflow_id_fk', - }).onDelete('cascade'), - foreignKey({ - columns: [table.userId], - foreignColumns: [user.id], - name: 'chat_user_id_user_id_fk', - }).onDelete('cascade'), - ] -) - -export const workspace = pgTable( - 'workspace', - { - id: text().primaryKey().notNull(), - name: text().notNull(), - ownerId: text('owner_id').notNull(), - createdAt: timestamp('created_at', { mode: 'string' }).defaultNow().notNull(), - updatedAt: timestamp('updated_at', { mode: 'string' }).defaultNow().notNull(), - }, - (table) => [ - foreignKey({ - columns: [table.ownerId], - foreignColumns: [user.id], - name: 'workspace_owner_id_user_id_fk', - }).onDelete('cascade'), - ] -) - -export const organization = pgTable('organization', { - id: text().primaryKey().notNull(), - name: text().notNull(), - slug: text().notNull(), - logo: text(), - metadata: json(), - createdAt: timestamp('created_at', { mode: 'string' }).defaultNow().notNull(), - updatedAt: timestamp('updated_at', { mode: 'string' }).defaultNow().notNull(), -}) - -export const memory = pgTable( - 'memory', - { - id: text().primaryKey().notNull(), - workflowId: text('workflow_id'), - key: text().notNull(), - type: text().notNull(), - data: json().notNull(), - createdAt: timestamp('created_at', { mode: 'string' }).defaultNow().notNull(), - updatedAt: timestamp('updated_at', { mode: 'string' }).defaultNow().notNull(), - deletedAt: timestamp('deleted_at', { mode: 'string' }), - }, - (table) => [ - index('memory_key_idx').using('btree', table.key.asc().nullsLast().op('text_ops')), - index('memory_workflow_idx').using('btree', table.workflowId.asc().nullsLast().op('text_ops')), - uniqueIndex('memory_workflow_key_idx').using( - 'btree', - table.workflowId.asc().nullsLast().op('text_ops'), - table.key.asc().nullsLast().op('text_ops') - ), - foreignKey({ - columns: [table.workflowId], - foreignColumns: [workflow.id], - name: 'memory_workflow_id_workflow_id_fk', - }).onDelete('cascade'), - ] -) - -export const knowledgeBase = pgTable( - 'knowledge_base', - { - id: text().primaryKey().notNull(), - userId: text('user_id').notNull(), - workspaceId: text('workspace_id'), - name: text().notNull(), - description: text(), - tokenCount: integer('token_count').default(0).notNull(), - embeddingModel: text('embedding_model').default('text-embedding-3-small').notNull(), - embeddingDimension: integer('embedding_dimension').default(1536).notNull(), - chunkingConfig: json('chunking_config') - .default({ maxSize: 1024, minSize: 100, overlap: 200 }) - .notNull(), - deletedAt: timestamp('deleted_at', { mode: 'string' }), - createdAt: timestamp('created_at', { mode: 'string' }).defaultNow().notNull(), - updatedAt: timestamp('updated_at', { mode: 'string' }).defaultNow().notNull(), - }, - (table) => [ - index('kb_deleted_at_idx').using( - 'btree', - table.deletedAt.asc().nullsLast().op('timestamp_ops') - ), - index('kb_user_id_idx').using('btree', table.userId.asc().nullsLast().op('text_ops')), - index('kb_user_workspace_idx').using( - 'btree', - table.userId.asc().nullsLast().op('text_ops'), - table.workspaceId.asc().nullsLast().op('text_ops') - ), - index('kb_workspace_id_idx').using('btree', table.workspaceId.asc().nullsLast().op('text_ops')), - foreignKey({ - columns: [table.userId], - foreignColumns: [user.id], - name: 'knowledge_base_user_id_user_id_fk', - }).onDelete('cascade'), - foreignKey({ - columns: [table.workspaceId], - foreignColumns: [workspace.id], - name: 'knowledge_base_workspace_id_workspace_id_fk', - }).onDelete('cascade'), - ] -) - -export const workflow = pgTable( - 'workflow', - { - id: text().primaryKey().notNull(), - userId: text('user_id').notNull(), - name: text().notNull(), - description: text(), - state: json().notNull(), - lastSynced: timestamp('last_synced', { mode: 'string' }).notNull(), - createdAt: timestamp('created_at', { mode: 'string' }).notNull(), - updatedAt: timestamp('updated_at', { mode: 'string' }).notNull(), - isDeployed: boolean('is_deployed').default(false).notNull(), - deployedAt: timestamp('deployed_at', { mode: 'string' }), - color: text().default('#3972F6').notNull(), - collaborators: json().default([]).notNull(), - isPublished: boolean('is_published').default(false).notNull(), - runCount: integer('run_count').default(0).notNull(), - lastRunAt: timestamp('last_run_at', { mode: 'string' }), - variables: json().default({}), - marketplaceData: json('marketplace_data'), - deployedState: json('deployed_state'), - workspaceId: text('workspace_id'), - folderId: text('folder_id'), - }, - (table) => [ - foreignKey({ - columns: [table.userId], - foreignColumns: [user.id], - name: 'workflow_user_id_user_id_fk', - }).onDelete('cascade'), - foreignKey({ - columns: [table.workspaceId], - foreignColumns: [workspace.id], - name: 'workflow_workspace_id_workspace_id_fk', - }).onDelete('cascade'), - foreignKey({ - columns: [table.folderId], - foreignColumns: [workflowFolder.id], - name: 'workflow_folder_id_workflow_folder_id_fk', - }).onDelete('set null'), - ] -) - -export const workflowFolder = pgTable( - 'workflow_folder', - { - id: text().primaryKey().notNull(), - name: text().notNull(), - userId: text('user_id').notNull(), - workspaceId: text('workspace_id').notNull(), - parentId: text('parent_id'), - color: text().default('#6B7280'), - isExpanded: boolean('is_expanded').default(true).notNull(), - sortOrder: integer('sort_order').default(0).notNull(), - createdAt: timestamp('created_at', { mode: 'string' }).defaultNow().notNull(), - updatedAt: timestamp('updated_at', { mode: 'string' }).defaultNow().notNull(), - }, - (table) => [ - index('workflow_folder_parent_sort_idx').using( - 'btree', - table.parentId.asc().nullsLast().op('int4_ops'), - table.sortOrder.asc().nullsLast().op('text_ops') - ), - index('workflow_folder_user_idx').using('btree', table.userId.asc().nullsLast().op('text_ops')), - index('workflow_folder_workspace_parent_idx').using( - 'btree', - table.workspaceId.asc().nullsLast().op('text_ops'), - table.parentId.asc().nullsLast().op('text_ops') - ), - foreignKey({ - columns: [table.userId], - foreignColumns: [user.id], - name: 'workflow_folder_user_id_user_id_fk', - }).onDelete('cascade'), - foreignKey({ - columns: [table.workspaceId], - foreignColumns: [workspace.id], - name: 'workflow_folder_workspace_id_workspace_id_fk', - }).onDelete('cascade'), - ] -) - -export const workflowEdges = pgTable( - 'workflow_edges', - { - id: text().primaryKey().notNull(), - workflowId: text('workflow_id').notNull(), - sourceBlockId: text('source_block_id').notNull(), - targetBlockId: text('target_block_id').notNull(), - sourceHandle: text('source_handle'), - targetHandle: text('target_handle'), - createdAt: timestamp('created_at', { mode: 'string' }).defaultNow().notNull(), - }, - (table) => [ - index('workflow_edges_source_block_idx').using( - 'btree', - table.sourceBlockId.asc().nullsLast().op('text_ops') - ), - index('workflow_edges_target_block_idx').using( - 'btree', - table.targetBlockId.asc().nullsLast().op('text_ops') - ), - index('workflow_edges_workflow_id_idx').using( - 'btree', - table.workflowId.asc().nullsLast().op('text_ops') - ), - index('workflow_edges_workflow_source_idx').using( - 'btree', - table.workflowId.asc().nullsLast().op('text_ops'), - table.sourceBlockId.asc().nullsLast().op('text_ops') - ), - index('workflow_edges_workflow_target_idx').using( - 'btree', - table.workflowId.asc().nullsLast().op('text_ops'), - table.targetBlockId.asc().nullsLast().op('text_ops') - ), - foreignKey({ - columns: [table.workflowId], - foreignColumns: [workflow.id], - name: 'workflow_edges_workflow_id_workflow_id_fk', - }).onDelete('cascade'), - foreignKey({ - columns: [table.sourceBlockId], - foreignColumns: [workflowBlocks.id], - name: 'workflow_edges_source_block_id_workflow_blocks_id_fk', - }).onDelete('cascade'), - foreignKey({ - columns: [table.targetBlockId], - foreignColumns: [workflowBlocks.id], - name: 'workflow_edges_target_block_id_workflow_blocks_id_fk', - }).onDelete('cascade'), - ] -) - -export const workflowSubflows = pgTable( - 'workflow_subflows', - { - id: text().primaryKey().notNull(), - workflowId: text('workflow_id').notNull(), - type: text().notNull(), - config: jsonb().default({}).notNull(), - createdAt: timestamp('created_at', { mode: 'string' }).defaultNow().notNull(), - updatedAt: timestamp('updated_at', { mode: 'string' }).defaultNow().notNull(), - }, - (table) => [ - index('workflow_subflows_workflow_id_idx').using( - 'btree', - table.workflowId.asc().nullsLast().op('text_ops') - ), - index('workflow_subflows_workflow_type_idx').using( - 'btree', - table.workflowId.asc().nullsLast().op('text_ops'), - table.type.asc().nullsLast().op('text_ops') - ), - foreignKey({ - columns: [table.workflowId], - foreignColumns: [workflow.id], - name: 'workflow_subflows_workflow_id_workflow_id_fk', - }).onDelete('cascade'), - ] -) - -export const workspaceInvitation = pgTable( - 'workspace_invitation', - { - id: text().primaryKey().notNull(), - workspaceId: text('workspace_id').notNull(), - email: text().notNull(), - inviterId: text('inviter_id').notNull(), - role: text().default('member').notNull(), - status: text().default('pending').notNull(), - token: text().notNull(), - expiresAt: timestamp('expires_at', { mode: 'string' }).notNull(), - createdAt: timestamp('created_at', { mode: 'string' }).defaultNow().notNull(), - updatedAt: timestamp('updated_at', { mode: 'string' }).defaultNow().notNull(), - permissions: permissionType().default('admin').notNull(), - }, - (table) => [ - foreignKey({ - columns: [table.workspaceId], - foreignColumns: [workspace.id], - name: 'workspace_invitation_workspace_id_workspace_id_fk', - }).onDelete('cascade'), - foreignKey({ - columns: [table.inviterId], - foreignColumns: [user.id], - name: 'workspace_invitation_inviter_id_user_id_fk', - }).onDelete('cascade'), - unique('workspace_invitation_token_unique').on(table.token), - ] -) - -export const permissions = pgTable( - 'permissions', - { - id: text().primaryKey().notNull(), - userId: text('user_id').notNull(), - entityType: text('entity_type').notNull(), - entityId: text('entity_id').notNull(), - permissionType: permissionType('permission_type').notNull(), - createdAt: timestamp('created_at', { mode: 'string' }).defaultNow().notNull(), - updatedAt: timestamp('updated_at', { mode: 'string' }).defaultNow().notNull(), - }, - (table) => [ - index('permissions_entity_idx').using( - 'btree', - table.entityType.asc().nullsLast().op('text_ops'), - table.entityId.asc().nullsLast().op('text_ops') - ), - uniqueIndex('permissions_unique_constraint').using( - 'btree', - table.userId.asc().nullsLast().op('text_ops'), - table.entityType.asc().nullsLast().op('text_ops'), - table.entityId.asc().nullsLast().op('text_ops') - ), - index('permissions_user_entity_idx').using( - 'btree', - table.userId.asc().nullsLast().op('text_ops'), - table.entityType.asc().nullsLast().op('text_ops'), - table.entityId.asc().nullsLast().op('text_ops') - ), - index('permissions_user_entity_permission_idx').using( - 'btree', - table.userId.asc().nullsLast().op('text_ops'), - table.entityType.asc().nullsLast().op('text_ops'), - table.permissionType.asc().nullsLast().op('enum_ops') - ), - index('permissions_user_entity_type_idx').using( - 'btree', - table.userId.asc().nullsLast().op('text_ops'), - table.entityType.asc().nullsLast().op('text_ops') - ), - index('permissions_user_id_idx').using('btree', table.userId.asc().nullsLast().op('text_ops')), - foreignKey({ - columns: [table.userId], - foreignColumns: [user.id], - name: 'permissions_user_id_user_id_fk', - }).onDelete('cascade'), - ] -) - -export const workflowBlocks = pgTable( - 'workflow_blocks', - { - id: text().primaryKey().notNull(), - workflowId: text('workflow_id').notNull(), - type: text().notNull(), - name: text().notNull(), - positionX: numeric('position_x').notNull(), - positionY: numeric('position_y').notNull(), - enabled: boolean().default(true).notNull(), - horizontalHandles: boolean('horizontal_handles').default(true).notNull(), - isWide: boolean('is_wide').default(false).notNull(), - height: numeric().default('0').notNull(), - subBlocks: jsonb('sub_blocks').default({}).notNull(), - outputs: jsonb().default({}).notNull(), - data: jsonb().default({}), - parentId: text('parent_id'), - extent: text(), - createdAt: timestamp('created_at', { mode: 'string' }).defaultNow().notNull(), - updatedAt: timestamp('updated_at', { mode: 'string' }).defaultNow().notNull(), - advancedMode: boolean('advanced_mode').default(false).notNull(), - }, - (table) => [ - index('workflow_blocks_parent_id_idx').using( - 'btree', - table.parentId.asc().nullsLast().op('text_ops') - ), - index('workflow_blocks_workflow_id_idx').using( - 'btree', - table.workflowId.asc().nullsLast().op('text_ops') - ), - index('workflow_blocks_workflow_parent_idx').using( - 'btree', - table.workflowId.asc().nullsLast().op('text_ops'), - table.parentId.asc().nullsLast().op('text_ops') - ), - index('workflow_blocks_workflow_type_idx').using( - 'btree', - table.workflowId.asc().nullsLast().op('text_ops'), - table.type.asc().nullsLast().op('text_ops') - ), - foreignKey({ - columns: [table.workflowId], - foreignColumns: [workflow.id], - name: 'workflow_blocks_workflow_id_workflow_id_fk', - }).onDelete('cascade'), - ] -) - -export const userStats = pgTable( - 'user_stats', - { - id: text().primaryKey().notNull(), - userId: text('user_id').notNull(), - totalManualExecutions: integer('total_manual_executions').default(0).notNull(), - totalApiCalls: integer('total_api_calls').default(0).notNull(), - totalWebhookTriggers: integer('total_webhook_triggers').default(0).notNull(), - totalScheduledExecutions: integer('total_scheduled_executions').default(0).notNull(), - totalTokensUsed: integer('total_tokens_used').default(0).notNull(), - totalCost: numeric('total_cost').default('0').notNull(), - lastActive: timestamp('last_active', { mode: 'string' }).defaultNow().notNull(), - totalChatExecutions: integer('total_chat_executions').default(0).notNull(), - currentUsageLimit: numeric('current_usage_limit').default('5').notNull(), - usageLimitSetBy: text('usage_limit_set_by'), - usageLimitUpdatedAt: timestamp('usage_limit_updated_at', { mode: 'string' }).defaultNow(), - currentPeriodCost: numeric('current_period_cost').default('0').notNull(), - billingPeriodStart: timestamp('billing_period_start', { mode: 'string' }).defaultNow(), - billingPeriodEnd: timestamp('billing_period_end', { mode: 'string' }), - lastPeriodCost: numeric('last_period_cost').default('0'), - }, - (table) => [ - foreignKey({ - columns: [table.userId], - foreignColumns: [user.id], - name: 'user_stats_user_id_user_id_fk', - }).onDelete('cascade'), - unique('user_stats_user_id_unique').on(table.userId), - ] -) - -export const subscription = pgTable( - 'subscription', - { - id: text().primaryKey().notNull(), - plan: text().notNull(), - referenceId: text('reference_id').notNull(), - stripeCustomerId: text('stripe_customer_id'), - stripeSubscriptionId: text('stripe_subscription_id'), - status: text(), - periodStart: timestamp('period_start', { mode: 'string' }), - periodEnd: timestamp('period_end', { mode: 'string' }), - cancelAtPeriodEnd: boolean('cancel_at_period_end'), - seats: integer(), - trialStart: timestamp('trial_start', { mode: 'string' }), - trialEnd: timestamp('trial_end', { mode: 'string' }), - metadata: json(), - }, - (table) => [ - index('subscription_reference_status_idx').using( - 'btree', - table.referenceId.asc().nullsLast().op('text_ops'), - table.status.asc().nullsLast().op('text_ops') - ), - check( - 'check_enterprise_metadata', - sql`(plan <> 'enterprise'::text) OR ((metadata IS NOT NULL) AND (((metadata ->> 'perSeatAllowance'::text) IS NOT NULL) OR ((metadata ->> 'totalAllowance'::text) IS NOT NULL)))` - ), - ] -) - -export const workflowExecutionBlocks = pgTable( - 'workflow_execution_blocks', - { - id: text().primaryKey().notNull(), - executionId: text('execution_id').notNull(), - workflowId: text('workflow_id').notNull(), - blockId: text('block_id').notNull(), - blockName: text('block_name'), - blockType: text('block_type').notNull(), - startedAt: timestamp('started_at', { mode: 'string' }).notNull(), - endedAt: timestamp('ended_at', { mode: 'string' }), - durationMs: integer('duration_ms'), - status: text().notNull(), - errorMessage: text('error_message'), - errorStackTrace: text('error_stack_trace'), - inputData: jsonb('input_data'), - outputData: jsonb('output_data'), - costInput: numeric('cost_input', { precision: 10, scale: 6 }), - costOutput: numeric('cost_output', { precision: 10, scale: 6 }), - costTotal: numeric('cost_total', { precision: 10, scale: 6 }), - tokensPrompt: integer('tokens_prompt'), - tokensCompletion: integer('tokens_completion'), - tokensTotal: integer('tokens_total'), - modelUsed: text('model_used'), - metadata: jsonb(), - createdAt: timestamp('created_at', { mode: 'string' }).defaultNow().notNull(), - }, - (table) => [ - index('execution_blocks_block_id_idx').using( - 'btree', - table.blockId.asc().nullsLast().op('text_ops') - ), - index('execution_blocks_cost_idx').using( - 'btree', - table.costTotal.asc().nullsLast().op('numeric_ops') - ), - index('execution_blocks_duration_idx').using( - 'btree', - table.durationMs.asc().nullsLast().op('int4_ops') - ), - index('execution_blocks_execution_id_idx').using( - 'btree', - table.executionId.asc().nullsLast().op('text_ops') - ), - index('execution_blocks_execution_status_idx').using( - 'btree', - table.executionId.asc().nullsLast().op('text_ops'), - table.status.asc().nullsLast().op('text_ops') - ), - index('execution_blocks_started_at_idx').using( - 'btree', - table.startedAt.asc().nullsLast().op('timestamp_ops') - ), - index('execution_blocks_status_idx').using( - 'btree', - table.status.asc().nullsLast().op('text_ops') - ), - index('execution_blocks_workflow_execution_idx').using( - 'btree', - table.workflowId.asc().nullsLast().op('text_ops'), - table.executionId.asc().nullsLast().op('text_ops') - ), - index('execution_blocks_workflow_id_idx').using( - 'btree', - table.workflowId.asc().nullsLast().op('text_ops') - ), - foreignKey({ - columns: [table.workflowId], - foreignColumns: [workflow.id], - name: 'workflow_execution_blocks_workflow_id_workflow_id_fk', - }).onDelete('cascade'), - ] -) - -export const workflowExecutionLogs = pgTable( - 'workflow_execution_logs', - { - id: text().primaryKey().notNull(), - workflowId: text('workflow_id').notNull(), - executionId: text('execution_id').notNull(), - stateSnapshotId: text('state_snapshot_id').notNull(), - level: text().notNull(), - message: text().notNull(), - trigger: text().notNull(), - startedAt: timestamp('started_at', { mode: 'string' }).notNull(), - endedAt: timestamp('ended_at', { mode: 'string' }), - totalDurationMs: integer('total_duration_ms'), - blockCount: integer('block_count').default(0).notNull(), - successCount: integer('success_count').default(0).notNull(), - errorCount: integer('error_count').default(0).notNull(), - skippedCount: integer('skipped_count').default(0).notNull(), - totalCost: numeric('total_cost', { precision: 10, scale: 6 }), - totalInputCost: numeric('total_input_cost', { precision: 10, scale: 6 }), - totalOutputCost: numeric('total_output_cost', { precision: 10, scale: 6 }), - totalTokens: integer('total_tokens'), - metadata: jsonb().default({}).notNull(), - createdAt: timestamp('created_at', { mode: 'string' }).defaultNow().notNull(), - }, - (table) => [ - index('workflow_execution_logs_cost_idx').using( - 'btree', - table.totalCost.asc().nullsLast().op('numeric_ops') - ), - index('workflow_execution_logs_duration_idx').using( - 'btree', - table.totalDurationMs.asc().nullsLast().op('int4_ops') - ), - index('workflow_execution_logs_execution_id_idx').using( - 'btree', - table.executionId.asc().nullsLast().op('text_ops') - ), - uniqueIndex('workflow_execution_logs_execution_id_unique').using( - 'btree', - table.executionId.asc().nullsLast().op('text_ops') - ), - index('workflow_execution_logs_level_idx').using( - 'btree', - table.level.asc().nullsLast().op('text_ops') - ), - index('workflow_execution_logs_started_at_idx').using( - 'btree', - table.startedAt.asc().nullsLast().op('timestamp_ops') - ), - index('workflow_execution_logs_trigger_idx').using( - 'btree', - table.trigger.asc().nullsLast().op('text_ops') - ), - index('workflow_execution_logs_workflow_id_idx').using( - 'btree', - table.workflowId.asc().nullsLast().op('text_ops') - ), - foreignKey({ - columns: [table.workflowId], - foreignColumns: [workflow.id], - name: 'workflow_execution_logs_workflow_id_workflow_id_fk', - }).onDelete('cascade'), - foreignKey({ - columns: [table.stateSnapshotId], - foreignColumns: [workflowExecutionSnapshots.id], - name: 'workflow_execution_logs_state_snapshot_id_workflow_execution_sn', - }), - ] -) - -export const workflowExecutionSnapshots = pgTable( - 'workflow_execution_snapshots', - { - id: text().primaryKey().notNull(), - workflowId: text('workflow_id').notNull(), - stateHash: text('state_hash').notNull(), - stateData: jsonb('state_data').notNull(), - createdAt: timestamp('created_at', { mode: 'string' }).defaultNow().notNull(), - }, - (table) => [ - index('workflow_snapshots_created_at_idx').using( - 'btree', - table.createdAt.asc().nullsLast().op('timestamp_ops') - ), - index('workflow_snapshots_hash_idx').using( - 'btree', - table.stateHash.asc().nullsLast().op('text_ops') - ), - uniqueIndex('workflow_snapshots_workflow_hash_idx').using( - 'btree', - table.workflowId.asc().nullsLast().op('text_ops'), - table.stateHash.asc().nullsLast().op('text_ops') - ), - index('workflow_snapshots_workflow_id_idx').using( - 'btree', - table.workflowId.asc().nullsLast().op('text_ops') - ), - foreignKey({ - columns: [table.workflowId], - foreignColumns: [workflow.id], - name: 'workflow_execution_snapshots_workflow_id_workflow_id_fk', - }).onDelete('cascade'), - ] -) - -export const copilotChats = pgTable( - 'copilot_chats', - { - id: uuid().defaultRandom().primaryKey().notNull(), - userId: text('user_id').notNull(), - workflowId: text('workflow_id').notNull(), - title: text(), - messages: jsonb().default([]).notNull(), - model: text().default('claude-3-7-sonnet-latest').notNull(), - createdAt: timestamp('created_at', { mode: 'string' }).defaultNow().notNull(), - updatedAt: timestamp('updated_at', { mode: 'string' }).defaultNow().notNull(), - previewYaml: text('preview_yaml'), - }, - (table) => [ - index('copilot_chats_created_at_idx').using( - 'btree', - table.createdAt.asc().nullsLast().op('timestamp_ops') - ), - index('copilot_chats_updated_at_idx').using( - 'btree', - table.updatedAt.asc().nullsLast().op('timestamp_ops') - ), - index('copilot_chats_user_id_idx').using( - 'btree', - table.userId.asc().nullsLast().op('text_ops') - ), - index('copilot_chats_user_workflow_idx').using( - 'btree', - table.userId.asc().nullsLast().op('text_ops'), - table.workflowId.asc().nullsLast().op('text_ops') - ), - index('copilot_chats_workflow_id_idx').using( - 'btree', - table.workflowId.asc().nullsLast().op('text_ops') - ), - foreignKey({ - columns: [table.userId], - foreignColumns: [user.id], - name: 'copilot_chats_user_id_user_id_fk', - }).onDelete('cascade'), - foreignKey({ - columns: [table.workflowId], - foreignColumns: [workflow.id], - name: 'copilot_chats_workflow_id_workflow_id_fk', - }).onDelete('cascade'), - ] -) - -export const docsEmbeddings = pgTable( - 'docs_embeddings', - { - chunkId: uuid('chunk_id').defaultRandom().primaryKey().notNull(), - chunkText: text('chunk_text').notNull(), - sourceDocument: text('source_document').notNull(), - sourceLink: text('source_link').notNull(), - headerText: text('header_text').notNull(), - headerLevel: integer('header_level').notNull(), - tokenCount: integer('token_count').notNull(), - embedding: vector({ dimensions: 1536 }).notNull(), - embeddingModel: text('embedding_model').default('text-embedding-3-small').notNull(), - metadata: jsonb().default({}).notNull(), - // tsvector column for full-text search - chunkTextTsv: tsvector('chunk_text_tsv').generatedAlwaysAs( - sql`to_tsvector('english'::regconfig, chunk_text)` - ), - createdAt: timestamp('created_at', { mode: 'string' }).defaultNow().notNull(), - updatedAt: timestamp('updated_at', { mode: 'string' }).defaultNow().notNull(), - }, - (table) => [ - index('docs_emb_chunk_text_fts_idx').using( - 'gin', - table.chunkTextTsv.asc().nullsLast().op('tsvector_ops') - ), - index('docs_emb_created_at_idx').using( - 'btree', - table.createdAt.asc().nullsLast().op('timestamp_ops') - ), - index('docs_emb_header_level_idx').using( - 'btree', - table.headerLevel.asc().nullsLast().op('int4_ops') - ), - index('docs_emb_metadata_gin_idx').using( - 'gin', - table.metadata.asc().nullsLast().op('jsonb_ops') - ), - index('docs_emb_model_idx').using( - 'btree', - table.embeddingModel.asc().nullsLast().op('text_ops') - ), - index('docs_emb_source_document_idx').using( - 'btree', - table.sourceDocument.asc().nullsLast().op('text_ops') - ), - index('docs_emb_source_header_idx').using( - 'btree', - table.sourceDocument.asc().nullsLast().op('text_ops'), - table.headerLevel.asc().nullsLast().op('int4_ops') - ), - index('docs_embedding_vector_hnsw_idx') - .using('hnsw', table.embedding.asc().nullsLast().op('vector_cosine_ops')) - .with({ m: '16', ef_construction: '64' }), - check('docs_embedding_not_null_check', sql`embedding IS NOT NULL`), - check('docs_header_level_check', sql`(header_level >= 1) AND (header_level <= 6)`), - ] -) - -export const document = pgTable( - 'document', - { - id: text().primaryKey().notNull(), - knowledgeBaseId: text('knowledge_base_id').notNull(), - filename: text().notNull(), - fileUrl: text('file_url').notNull(), - fileSize: integer('file_size').notNull(), - mimeType: text('mime_type').notNull(), - chunkCount: integer('chunk_count').default(0).notNull(), - tokenCount: integer('token_count').default(0).notNull(), - characterCount: integer('character_count').default(0).notNull(), - enabled: boolean().default(true).notNull(), - deletedAt: timestamp('deleted_at', { mode: 'string' }), - uploadedAt: timestamp('uploaded_at', { mode: 'string' }).defaultNow().notNull(), - tag1: text(), - tag2: text(), - tag3: text(), - tag4: text(), - tag5: text(), - tag6: text(), - tag7: text(), - processingStatus: text('processing_status').default('pending').notNull(), - processingStartedAt: timestamp('processing_started_at', { mode: 'string' }), - processingCompletedAt: timestamp('processing_completed_at', { mode: 'string' }), - processingError: text('processing_error'), - }, - (table) => [ - index('doc_filename_idx').using('btree', table.filename.asc().nullsLast().op('text_ops')), - index('doc_kb_id_idx').using('btree', table.knowledgeBaseId.asc().nullsLast().op('text_ops')), - index('doc_kb_uploaded_at_idx').using( - 'btree', - table.knowledgeBaseId.asc().nullsLast().op('text_ops'), - table.uploadedAt.asc().nullsLast().op('timestamp_ops') - ), - index('doc_processing_status_idx').using( - 'btree', - table.knowledgeBaseId.asc().nullsLast().op('text_ops'), - table.processingStatus.asc().nullsLast().op('text_ops') - ), - index('doc_tag1_idx').using('btree', table.tag1.asc().nullsLast().op('text_ops')), - index('doc_tag2_idx').using('btree', table.tag2.asc().nullsLast().op('text_ops')), - index('doc_tag3_idx').using('btree', table.tag3.asc().nullsLast().op('text_ops')), - index('doc_tag4_idx').using('btree', table.tag4.asc().nullsLast().op('text_ops')), - index('doc_tag5_idx').using('btree', table.tag5.asc().nullsLast().op('text_ops')), - index('doc_tag6_idx').using('btree', table.tag6.asc().nullsLast().op('text_ops')), - index('doc_tag7_idx').using('btree', table.tag7.asc().nullsLast().op('text_ops')), - foreignKey({ - columns: [table.knowledgeBaseId], - foreignColumns: [knowledgeBase.id], - name: 'document_knowledge_base_id_knowledge_base_id_fk', - }).onDelete('cascade'), - ] -) - -export const embedding = pgTable( - 'embedding', - { - id: text().primaryKey().notNull(), - knowledgeBaseId: text('knowledge_base_id').notNull(), - documentId: text('document_id').notNull(), - chunkIndex: integer('chunk_index').notNull(), - chunkHash: text('chunk_hash').notNull(), - content: text().notNull(), - contentLength: integer('content_length').notNull(), - tokenCount: integer('token_count').notNull(), - embedding: vector({ dimensions: 1536 }), - embeddingModel: text('embedding_model').default('text-embedding-3-small').notNull(), - startOffset: integer('start_offset').notNull(), - endOffset: integer('end_offset').notNull(), - createdAt: timestamp('created_at', { mode: 'string' }).defaultNow().notNull(), - updatedAt: timestamp('updated_at', { mode: 'string' }).defaultNow().notNull(), - // tsvector column for full-text search - contentTsv: tsvector('content_tsv').generatedAlwaysAs( - sql`to_tsvector('english'::regconfig, content)` - ), - enabled: boolean().default(true).notNull(), - tag1: text(), - tag2: text(), - tag3: text(), - tag4: text(), - tag5: text(), - tag6: text(), - tag7: text(), - }, - (table) => [ - index('emb_content_fts_idx').using( - 'gin', - table.contentTsv.asc().nullsLast().op('tsvector_ops') - ), - uniqueIndex('emb_doc_chunk_idx').using( - 'btree', - table.documentId.asc().nullsLast().op('int4_ops'), - table.chunkIndex.asc().nullsLast().op('text_ops') - ), - index('emb_doc_enabled_idx').using( - 'btree', - table.documentId.asc().nullsLast().op('bool_ops'), - table.enabled.asc().nullsLast().op('bool_ops') - ), - index('emb_doc_id_idx').using('btree', table.documentId.asc().nullsLast().op('text_ops')), - index('emb_kb_enabled_idx').using( - 'btree', - table.knowledgeBaseId.asc().nullsLast().op('text_ops'), - table.enabled.asc().nullsLast().op('text_ops') - ), - index('emb_kb_id_idx').using('btree', table.knowledgeBaseId.asc().nullsLast().op('text_ops')), - index('emb_kb_model_idx').using( - 'btree', - table.knowledgeBaseId.asc().nullsLast().op('text_ops'), - table.embeddingModel.asc().nullsLast().op('text_ops') - ), - index('emb_tag1_idx').using('btree', table.tag1.asc().nullsLast().op('text_ops')), - index('emb_tag2_idx').using('btree', table.tag2.asc().nullsLast().op('text_ops')), - index('emb_tag3_idx').using('btree', table.tag3.asc().nullsLast().op('text_ops')), - index('emb_tag4_idx').using('btree', table.tag4.asc().nullsLast().op('text_ops')), - index('emb_tag5_idx').using('btree', table.tag5.asc().nullsLast().op('text_ops')), - index('emb_tag6_idx').using('btree', table.tag6.asc().nullsLast().op('text_ops')), - index('emb_tag7_idx').using('btree', table.tag7.asc().nullsLast().op('text_ops')), - index('embedding_vector_hnsw_idx') - .using('hnsw', table.embedding.asc().nullsLast().op('vector_cosine_ops')) - .with({ m: '16', ef_construction: '64' }), - foreignKey({ - columns: [table.knowledgeBaseId], - foreignColumns: [knowledgeBase.id], - name: 'embedding_knowledge_base_id_knowledge_base_id_fk', - }).onDelete('cascade'), - foreignKey({ - columns: [table.documentId], - foreignColumns: [document.id], - name: 'embedding_document_id_document_id_fk', - }).onDelete('cascade'), - check('embedding_not_null_check', sql`embedding IS NOT NULL`), - ] -) - -export const templateStars = pgTable( - 'template_stars', - { - id: text().primaryKey().notNull(), - userId: text('user_id').notNull(), - templateId: text('template_id').notNull(), - starredAt: timestamp('starred_at', { mode: 'string' }).defaultNow().notNull(), - createdAt: timestamp('created_at', { mode: 'string' }).defaultNow().notNull(), - }, - (table) => [ - index('template_stars_starred_at_idx').using( - 'btree', - table.starredAt.asc().nullsLast().op('timestamp_ops') - ), - index('template_stars_template_id_idx').using( - 'btree', - table.templateId.asc().nullsLast().op('text_ops') - ), - index('template_stars_template_starred_at_idx').using( - 'btree', - table.templateId.asc().nullsLast().op('text_ops'), - table.starredAt.asc().nullsLast().op('timestamp_ops') - ), - index('template_stars_template_user_idx').using( - 'btree', - table.templateId.asc().nullsLast().op('text_ops'), - table.userId.asc().nullsLast().op('text_ops') - ), - index('template_stars_user_id_idx').using( - 'btree', - table.userId.asc().nullsLast().op('text_ops') - ), - index('template_stars_user_template_idx').using( - 'btree', - table.userId.asc().nullsLast().op('text_ops'), - table.templateId.asc().nullsLast().op('text_ops') - ), - uniqueIndex('template_stars_user_template_unique').using( - 'btree', - table.userId.asc().nullsLast().op('text_ops'), - table.templateId.asc().nullsLast().op('text_ops') - ), - foreignKey({ - columns: [table.userId], - foreignColumns: [user.id], - name: 'template_stars_user_id_user_id_fk', - }).onDelete('cascade'), - foreignKey({ - columns: [table.templateId], - foreignColumns: [templates.id], - name: 'template_stars_template_id_templates_id_fk', - }).onDelete('cascade'), - ] -) - -export const templates = pgTable( - 'templates', - { - id: text().primaryKey().notNull(), - workflowId: text('workflow_id').notNull(), - userId: text('user_id').notNull(), - name: text().notNull(), - description: text(), - author: text().notNull(), - views: integer().default(0).notNull(), - stars: integer().default(0).notNull(), - color: text().default('#3972F6').notNull(), - icon: text().default('FileText').notNull(), - category: text().notNull(), - state: jsonb().notNull(), - createdAt: timestamp('created_at', { mode: 'string' }).defaultNow().notNull(), - updatedAt: timestamp('updated_at', { mode: 'string' }).defaultNow().notNull(), - }, - (table) => [ - index('templates_category_idx').using('btree', table.category.asc().nullsLast().op('text_ops')), - index('templates_category_stars_idx').using( - 'btree', - table.category.asc().nullsLast().op('int4_ops'), - table.stars.asc().nullsLast().op('text_ops') - ), - index('templates_category_views_idx').using( - 'btree', - table.category.asc().nullsLast().op('int4_ops'), - table.views.asc().nullsLast().op('text_ops') - ), - index('templates_created_at_idx').using( - 'btree', - table.createdAt.asc().nullsLast().op('timestamp_ops') - ), - index('templates_stars_idx').using('btree', table.stars.asc().nullsLast().op('int4_ops')), - index('templates_updated_at_idx').using( - 'btree', - table.updatedAt.asc().nullsLast().op('timestamp_ops') - ), - index('templates_user_category_idx').using( - 'btree', - table.userId.asc().nullsLast().op('text_ops'), - table.category.asc().nullsLast().op('text_ops') - ), - index('templates_user_id_idx').using('btree', table.userId.asc().nullsLast().op('text_ops')), - index('templates_views_idx').using('btree', table.views.asc().nullsLast().op('int4_ops')), - index('templates_workflow_id_idx').using( - 'btree', - table.workflowId.asc().nullsLast().op('text_ops') - ), - foreignKey({ - columns: [table.workflowId], - foreignColumns: [workflow.id], - name: 'templates_workflow_id_workflow_id_fk', - }), - foreignKey({ - columns: [table.userId], - foreignColumns: [user.id], - name: 'templates_user_id_user_id_fk', - }).onDelete('cascade'), - ] -) - -export const settings = pgTable( - 'settings', - { - id: text().primaryKey().notNull(), - userId: text('user_id').notNull(), - general: json().default({}).notNull(), - updatedAt: timestamp('updated_at', { mode: 'string' }).defaultNow().notNull(), - theme: text().default('system').notNull(), - autoConnect: boolean('auto_connect').default(true).notNull(), - autoFillEnvVars: boolean('auto_fill_env_vars').default(true).notNull(), - telemetryEnabled: boolean('telemetry_enabled').default(true).notNull(), - telemetryNotifiedUser: boolean('telemetry_notified_user').default(false).notNull(), - emailPreferences: json('email_preferences').default({}).notNull(), - autoPan: boolean('auto_pan').default(true).notNull(), - consoleExpandedByDefault: boolean('console_expanded_by_default').default(true).notNull(), - }, - (table) => [ - foreignKey({ - columns: [table.userId], - foreignColumns: [user.id], - name: 'settings_user_id_user_id_fk', - }).onDelete('cascade'), - unique('settings_user_id_unique').on(table.userId), - ] -) - -export const userRateLimits = pgTable( - 'user_rate_limits', - { - userId: text('user_id').primaryKey().notNull(), - syncApiRequests: integer('sync_api_requests').default(0).notNull(), - asyncApiRequests: integer('async_api_requests').default(0).notNull(), - windowStart: timestamp('window_start', { mode: 'string' }).defaultNow().notNull(), - lastRequestAt: timestamp('last_request_at', { mode: 'string' }).defaultNow().notNull(), - isRateLimited: boolean('is_rate_limited').default(false).notNull(), - rateLimitResetAt: timestamp('rate_limit_reset_at', { mode: 'string' }), - }, - (table) => [ - foreignKey({ - columns: [table.userId], - foreignColumns: [user.id], - name: 'user_rate_limits_user_id_user_id_fk', - }).onDelete('cascade'), - ] -) - -export const webhook = pgTable( - 'webhook', - { - id: text().primaryKey().notNull(), - workflowId: text('workflow_id').notNull(), - path: text().notNull(), - provider: text(), - isActive: boolean('is_active').default(true).notNull(), - createdAt: timestamp('created_at', { mode: 'string' }).defaultNow().notNull(), - updatedAt: timestamp('updated_at', { mode: 'string' }).defaultNow().notNull(), - providerConfig: json('provider_config'), - blockId: text('block_id'), - }, - (table) => [ - uniqueIndex('path_idx').using('btree', table.path.asc().nullsLast().op('text_ops')), - foreignKey({ - columns: [table.workflowId], - foreignColumns: [workflow.id], - name: 'webhook_workflow_id_workflow_id_fk', - }).onDelete('cascade'), - foreignKey({ - columns: [table.blockId], - foreignColumns: [workflowBlocks.id], - name: 'webhook_block_id_workflow_blocks_id_fk', - }).onDelete('cascade'), - ] -) - -export const workflowSchedule = pgTable( - 'workflow_schedule', - { - id: text().primaryKey().notNull(), - workflowId: text('workflow_id').notNull(), - cronExpression: text('cron_expression'), - nextRunAt: timestamp('next_run_at', { mode: 'string' }), - lastRanAt: timestamp('last_ran_at', { mode: 'string' }), - triggerType: text('trigger_type').notNull(), - timezone: text().default('UTC').notNull(), - createdAt: timestamp('created_at', { mode: 'string' }).defaultNow().notNull(), - updatedAt: timestamp('updated_at', { mode: 'string' }).defaultNow().notNull(), - failedCount: integer('failed_count').default(0).notNull(), - status: text().default('active').notNull(), - lastFailedAt: timestamp('last_failed_at', { mode: 'string' }), - blockId: text('block_id'), - }, - (table) => [ - uniqueIndex('workflow_schedule_workflow_block_unique').using( - 'btree', - table.workflowId.asc().nullsLast().op('text_ops'), - table.blockId.asc().nullsLast().op('text_ops') - ), - foreignKey({ - columns: [table.workflowId], - foreignColumns: [workflow.id], - name: 'workflow_schedule_workflow_id_workflow_id_fk', - }).onDelete('cascade'), - foreignKey({ - columns: [table.blockId], - foreignColumns: [workflowBlocks.id], - name: 'workflow_schedule_block_id_workflow_blocks_id_fk', - }).onDelete('cascade'), - ] -) - -export const copilotCheckpoints = pgTable( - 'copilot_checkpoints', - { - id: uuid().defaultRandom().primaryKey().notNull(), - userId: text('user_id').notNull(), - workflowId: text('workflow_id').notNull(), - chatId: uuid('chat_id').notNull(), - yaml: text().notNull(), - createdAt: timestamp('created_at', { mode: 'string' }).defaultNow().notNull(), - updatedAt: timestamp('updated_at', { mode: 'string' }).defaultNow().notNull(), - }, - (table) => [ - index('copilot_checkpoints_chat_created_at_idx').using( - 'btree', - table.chatId.asc().nullsLast().op('uuid_ops'), - table.createdAt.asc().nullsLast().op('uuid_ops') - ), - index('copilot_checkpoints_chat_id_idx').using( - 'btree', - table.chatId.asc().nullsLast().op('uuid_ops') - ), - index('copilot_checkpoints_created_at_idx').using( - 'btree', - table.createdAt.asc().nullsLast().op('timestamp_ops') - ), - index('copilot_checkpoints_user_id_idx').using( - 'btree', - table.userId.asc().nullsLast().op('text_ops') - ), - index('copilot_checkpoints_user_workflow_idx').using( - 'btree', - table.userId.asc().nullsLast().op('text_ops'), - table.workflowId.asc().nullsLast().op('text_ops') - ), - index('copilot_checkpoints_workflow_chat_idx').using( - 'btree', - table.workflowId.asc().nullsLast().op('text_ops'), - table.chatId.asc().nullsLast().op('uuid_ops') - ), - index('copilot_checkpoints_workflow_id_idx').using( - 'btree', - table.workflowId.asc().nullsLast().op('text_ops') - ), - foreignKey({ - columns: [table.userId], - foreignColumns: [user.id], - name: 'copilot_checkpoints_user_id_user_id_fk', - }).onDelete('cascade'), - foreignKey({ - columns: [table.workflowId], - foreignColumns: [workflow.id], - name: 'copilot_checkpoints_workflow_id_workflow_id_fk', - }).onDelete('cascade'), - foreignKey({ - columns: [table.chatId], - foreignColumns: [copilotChats.id], - name: 'copilot_checkpoints_chat_id_copilot_chats_id_fk', - }).onDelete('cascade'), - ] -) diff --git a/apps/sim/db/schema.ts b/apps/sim/db/schema.ts index fd8224620e..fec8343fd7 100644 --- a/apps/sim/db/schema.ts +++ b/apps/sim/db/schema.ts @@ -162,6 +162,7 @@ export const workflowBlocks = pgTable( horizontalHandles: boolean('horizontal_handles').notNull().default(true), isWide: boolean('is_wide').notNull().default(false), advancedMode: boolean('advanced_mode').notNull().default(false), + triggerMode: boolean('trigger_mode').notNull().default(false), height: decimal('height').notNull().default('0'), subBlocks: jsonb('sub_blocks').notNull().default('{}'), @@ -300,6 +301,7 @@ export const workflowExecutionLogs = pgTable( totalTokens: integer('total_tokens'), metadata: jsonb('metadata').notNull().default('{}'), + files: jsonb('files'), // File metadata for execution files createdAt: timestamp('created_at').notNull().defaultNow(), }, (table) => ({ @@ -717,7 +719,7 @@ export const knowledgeBase = pgTable( // Chunking configuration stored as JSON for flexibility chunkingConfig: json('chunking_config') .notNull() - .default('{"maxSize": 1024, "minSize": 100, "overlap": 200}'), + .default('{"maxSize": 1024, "minSize": 1, "overlap": 200}'), // Soft delete support deletedAt: timestamp('deleted_at'), diff --git a/apps/sim/executor/__test-utils__/executor-mocks.ts b/apps/sim/executor/__test-utils__/executor-mocks.ts index 94dda1279c..06941d6363 100644 --- a/apps/sim/executor/__test-utils__/executor-mocks.ts +++ b/apps/sim/executor/__test-utils__/executor-mocks.ts @@ -8,7 +8,7 @@ export const createMockHandler = ( handlerName: string, options?: { canHandleCondition?: (block: any) => boolean - executeResult?: any + executeResult?: any | ((inputs: any) => any) } ) => { const defaultCanHandle = (block: any) => @@ -20,7 +20,12 @@ export const createMockHandler = ( return vi.fn().mockImplementation(() => ({ canHandle: options?.canHandleCondition || defaultCanHandle, - execute: vi.fn().mockResolvedValue(options?.executeResult || defaultExecuteResult), + execute: vi.fn().mockImplementation(async (block, inputs) => { + if (typeof options?.executeResult === 'function') { + return options.executeResult(inputs) + } + return options?.executeResult || defaultExecuteResult + }), })) } @@ -29,6 +34,11 @@ export const createMockHandler = ( */ export const setupHandlerMocks = () => { vi.doMock('@/executor/handlers', () => ({ + TriggerBlockHandler: createMockHandler('trigger', { + canHandleCondition: (block) => + block.metadata?.category === 'triggers' || block.config?.params?.triggerMode === true, + executeResult: (inputs: any) => inputs || {}, + }), AgentBlockHandler: createMockHandler('agent'), RouterBlockHandler: createMockHandler('router'), ConditionBlockHandler: createMockHandler('condition'), diff --git a/apps/sim/executor/handlers/agent/agent-handler.test.ts b/apps/sim/executor/handlers/agent/agent-handler.test.ts index c260c04895..ec1b9695b6 100644 --- a/apps/sim/executor/handlers/agent/agent-handler.test.ts +++ b/apps/sim/executor/handlers/agent/agent-handler.test.ts @@ -364,16 +364,23 @@ describe('AgentBlockHandler', () => { expect.objectContaining({ code: 'return { result: "auto tool executed", input }', input: 'test input', - }) + }), + false, // skipProxy + false, // skipPostProcess + expect.any(Object) // execution context ) await forceTool.executeFunction({ input: 'another test' }) - expect(mockExecuteTool).toHaveBeenCalledWith( + expect(mockExecuteTool).toHaveBeenNthCalledWith( + 2, // Check the 2nd call 'function_execute', expect.objectContaining({ code: 'return { result: "force tool executed", input }', input: 'another test', - }) + }), + false, // skipProxy + false, // skipPostProcess + expect.any(Object) // execution context ) const fetchCall = mockFetch.mock.calls[0] diff --git a/apps/sim/executor/handlers/agent/agent-handler.ts b/apps/sim/executor/handlers/agent/agent-handler.ts index 9e64a95e69..085ab6c849 100644 --- a/apps/sim/executor/handlers/agent/agent-handler.ts +++ b/apps/sim/executor/handlers/agent/agent-handler.ts @@ -172,14 +172,20 @@ export class AgentBlockHandler implements BlockHandler { // Merge user-provided parameters with LLM-generated parameters const mergedParams = mergeToolParameters(userProvidedParams, callParams) - const result = await executeTool('function_execute', { - code: tool.code, - ...mergedParams, - timeout: tool.timeout ?? DEFAULT_FUNCTION_TIMEOUT, - envVars: context.environmentVariables || {}, - isCustomTool: true, - _context: { workflowId: context.workflowId }, - }) + const result = await executeTool( + 'function_execute', + { + code: tool.code, + ...mergedParams, + timeout: tool.timeout ?? DEFAULT_FUNCTION_TIMEOUT, + envVars: context.environmentVariables || {}, + isCustomTool: true, + _context: { workflowId: context.workflowId }, + }, + false, // skipProxy + false, // skipPostProcess + context // execution context for file processing + ) if (!result.success) { throw new Error(result.error || 'Function execution failed') diff --git a/apps/sim/executor/handlers/api/api-handler.test.ts b/apps/sim/executor/handlers/api/api-handler.test.ts index a689eb205c..0aa9e112bb 100644 --- a/apps/sim/executor/handlers/api/api-handler.test.ts +++ b/apps/sim/executor/handlers/api/api-handler.test.ts @@ -100,11 +100,17 @@ describe('ApiBlockHandler', () => { const result = await handler.execute(mockBlock, inputs, mockContext) expect(mockGetTool).toHaveBeenCalledWith('http_request') - expect(mockExecuteTool).toHaveBeenCalledWith('http_request', { - ...inputs, - body: { key: 'value' }, // Expect parsed body - _context: { workflowId: 'test-workflow-id' }, - }) + expect(mockExecuteTool).toHaveBeenCalledWith( + 'http_request', + { + ...inputs, + body: { key: 'value' }, // Expect parsed body + _context: { workflowId: 'test-workflow-id' }, + }, + false, // skipProxy + false, // skipPostProcess + mockContext // execution context + ) expect(result).toEqual(expectedOutput) }) @@ -152,7 +158,10 @@ describe('ApiBlockHandler', () => { expect(mockExecuteTool).toHaveBeenCalledWith( 'http_request', - expect.objectContaining({ body: expectedParsedBody }) + expect.objectContaining({ body: expectedParsedBody }), + false, // skipProxy + false, // skipPostProcess + mockContext // execution context ) }) @@ -166,7 +175,10 @@ describe('ApiBlockHandler', () => { expect(mockExecuteTool).toHaveBeenCalledWith( 'http_request', - expect.objectContaining({ body: 'This is plain text' }) + expect.objectContaining({ body: 'This is plain text' }), + false, // skipProxy + false, // skipPostProcess + mockContext // execution context ) }) @@ -180,7 +192,10 @@ describe('ApiBlockHandler', () => { expect(mockExecuteTool).toHaveBeenCalledWith( 'http_request', - expect.objectContaining({ body: undefined }) + expect.objectContaining({ body: undefined }), + false, // skipProxy + false, // skipPostProcess + mockContext // execution context ) }) diff --git a/apps/sim/executor/handlers/api/api-handler.ts b/apps/sim/executor/handlers/api/api-handler.ts index 056e456139..c6318ffb0e 100644 --- a/apps/sim/executor/handlers/api/api-handler.ts +++ b/apps/sim/executor/handlers/api/api-handler.ts @@ -93,10 +93,16 @@ export class ApiBlockHandler implements BlockHandler { JSON.stringify(processedInputs.body, null, 2) ) - const result = await executeTool(block.config.tool, { - ...processedInputs, - _context: { workflowId: context.workflowId }, - }) + const result = await executeTool( + block.config.tool, + { + ...processedInputs, + _context: { workflowId: context.workflowId }, + }, + false, // skipProxy + false, // skipPostProcess + context // execution context for file processing + ) if (!result.success) { const errorDetails = [] diff --git a/apps/sim/executor/handlers/function/function-handler.test.ts b/apps/sim/executor/handlers/function/function-handler.test.ts index 9adb7304f4..c2deba29ee 100644 --- a/apps/sim/executor/handlers/function/function-handler.test.ts +++ b/apps/sim/executor/handlers/function/function-handler.test.ts @@ -85,7 +85,13 @@ describe('FunctionBlockHandler', () => { const result = await handler.execute(mockBlock, inputs, mockContext) - expect(mockExecuteTool).toHaveBeenCalledWith('function_execute', expectedToolParams) + expect(mockExecuteTool).toHaveBeenCalledWith( + 'function_execute', + expectedToolParams, + false, // skipProxy + false, // skipPostProcess + mockContext // execution context + ) expect(result).toEqual(expectedOutput) }) @@ -110,7 +116,13 @@ describe('FunctionBlockHandler', () => { const result = await handler.execute(mockBlock, inputs, mockContext) - expect(mockExecuteTool).toHaveBeenCalledWith('function_execute', expectedToolParams) + expect(mockExecuteTool).toHaveBeenCalledWith( + 'function_execute', + expectedToolParams, + false, // skipProxy + false, // skipPostProcess + mockContext // execution context + ) expect(result).toEqual(expectedOutput) }) @@ -127,7 +139,13 @@ describe('FunctionBlockHandler', () => { await handler.execute(mockBlock, inputs, mockContext) - expect(mockExecuteTool).toHaveBeenCalledWith('function_execute', expectedToolParams) + expect(mockExecuteTool).toHaveBeenCalledWith( + 'function_execute', + expectedToolParams, + false, // skipProxy + false, // skipPostProcess + mockContext // execution context + ) }) it('should handle execution errors from the tool', async () => { diff --git a/apps/sim/executor/handlers/function/function-handler.ts b/apps/sim/executor/handlers/function/function-handler.ts index 9b52a8e5b3..95c0fe4ca4 100644 --- a/apps/sim/executor/handlers/function/function-handler.ts +++ b/apps/sim/executor/handlers/function/function-handler.ts @@ -40,14 +40,20 @@ export class FunctionBlockHandler implements BlockHandler { } // Directly use the function_execute tool which calls the API route - const result = await executeTool('function_execute', { - code: codeContent, - timeout: inputs.timeout || 5000, - envVars: context.environmentVariables || {}, - blockData: blockData, // Pass block data for variable resolution - blockNameMapping: blockNameMapping, // Pass block name to ID mapping - _context: { workflowId: context.workflowId }, - }) + const result = await executeTool( + 'function_execute', + { + code: codeContent, + timeout: inputs.timeout || 5000, + envVars: context.environmentVariables || {}, + blockData: blockData, // Pass block data for variable resolution + blockNameMapping: blockNameMapping, // Pass block name to ID mapping + _context: { workflowId: context.workflowId }, + }, + false, // skipProxy + false, // skipPostProcess + context // execution context for file processing + ) if (!result.success) { throw new Error(result.error || 'Function execution failed') diff --git a/apps/sim/executor/handlers/generic/generic-handler.test.ts b/apps/sim/executor/handlers/generic/generic-handler.test.ts index ca53dbf296..c71bdb6d66 100644 --- a/apps/sim/executor/handlers/generic/generic-handler.test.ts +++ b/apps/sim/executor/handlers/generic/generic-handler.test.ts @@ -93,7 +93,13 @@ describe('GenericBlockHandler', () => { const result = await handler.execute(mockBlock, inputs, mockContext) expect(mockGetTool).toHaveBeenCalledWith('some_custom_tool') - expect(mockExecuteTool).toHaveBeenCalledWith('some_custom_tool', expectedToolParams) + expect(mockExecuteTool).toHaveBeenCalledWith( + 'some_custom_tool', + expectedToolParams, + false, // skipProxy + false, // skipPostProcess + mockContext // execution context + ) expect(result).toEqual(expectedOutput) }) diff --git a/apps/sim/executor/handlers/generic/generic-handler.ts b/apps/sim/executor/handlers/generic/generic-handler.ts index df88ee5ac7..85654871cf 100644 --- a/apps/sim/executor/handlers/generic/generic-handler.ts +++ b/apps/sim/executor/handlers/generic/generic-handler.ts @@ -29,10 +29,16 @@ export class GenericBlockHandler implements BlockHandler { } try { - const result = await executeTool(block.config.tool, { - ...inputs, - _context: { workflowId: context.workflowId }, - }) + const result = await executeTool( + block.config.tool, + { + ...inputs, + _context: { workflowId: context.workflowId }, + }, + false, // skipProxy + false, // skipPostProcess + context // execution context for file processing + ) if (!result.success) { const errorDetails = [] diff --git a/apps/sim/executor/handlers/index.ts b/apps/sim/executor/handlers/index.ts index 20c65456ef..c2acc8a39f 100644 --- a/apps/sim/executor/handlers/index.ts +++ b/apps/sim/executor/handlers/index.ts @@ -8,6 +8,7 @@ import { LoopBlockHandler } from '@/executor/handlers/loop/loop-handler' import { ParallelBlockHandler } from '@/executor/handlers/parallel/parallel-handler' import { ResponseBlockHandler } from '@/executor/handlers/response/response-handler' import { RouterBlockHandler } from '@/executor/handlers/router/router-handler' +import { TriggerBlockHandler } from '@/executor/handlers/trigger/trigger-handler' import { WorkflowBlockHandler } from '@/executor/handlers/workflow/workflow-handler' export { @@ -21,5 +22,6 @@ export { ParallelBlockHandler, ResponseBlockHandler, RouterBlockHandler, + TriggerBlockHandler, WorkflowBlockHandler, } diff --git a/apps/sim/executor/handlers/trigger/trigger-handler.test.ts b/apps/sim/executor/handlers/trigger/trigger-handler.test.ts new file mode 100644 index 0000000000..196a038c2e --- /dev/null +++ b/apps/sim/executor/handlers/trigger/trigger-handler.test.ts @@ -0,0 +1,323 @@ +import '@/executor/__test-utils__/mock-dependencies' + +import { beforeEach, describe, expect, it } from 'vitest' +import { TriggerBlockHandler } from '@/executor/handlers/trigger/trigger-handler' +import type { ExecutionContext } from '@/executor/types' +import type { SerializedBlock } from '@/serializer/types' + +describe('TriggerBlockHandler', () => { + let handler: TriggerBlockHandler + let mockContext: ExecutionContext + + beforeEach(() => { + handler = new TriggerBlockHandler() + + mockContext = { + workflowId: 'test-workflow-id', + blockStates: new Map(), + blockLogs: [], + metadata: { duration: 0 }, + environmentVariables: {}, + decisions: { router: new Map(), condition: new Map() }, + loopIterations: new Map(), + loopItems: new Map(), + executedBlocks: new Set(), + activeExecutionPath: new Set(), + completedLoops: new Set(), + } + }) + + describe('canHandle', () => { + it.concurrent('should handle blocks with triggers category', () => { + const triggerBlock: SerializedBlock = { + id: 'trigger-1', + metadata: { id: 'schedule', name: 'Schedule Block', category: 'triggers' }, + position: { x: 0, y: 0 }, + config: { tool: 'schedule', params: {} }, + inputs: {}, + outputs: {}, + enabled: true, + } + + expect(handler.canHandle(triggerBlock)).toBe(true) + }) + + it.concurrent('should handle blocks with triggerMode enabled', () => { + const gmailTriggerBlock: SerializedBlock = { + id: 'gmail-1', + metadata: { id: 'gmail', name: 'Gmail Block', category: 'tools' }, + position: { x: 0, y: 0 }, + config: { tool: 'gmail', params: { triggerMode: true } }, + inputs: {}, + outputs: {}, + enabled: true, + } + + expect(handler.canHandle(gmailTriggerBlock)).toBe(true) + }) + + it.concurrent('should not handle regular tool blocks without triggerMode', () => { + const toolBlock: SerializedBlock = { + id: 'tool-1', + metadata: { id: 'gmail', name: 'Gmail Block', category: 'tools' }, + position: { x: 0, y: 0 }, + config: { tool: 'gmail', params: { triggerMode: false } }, + inputs: {}, + outputs: {}, + enabled: true, + } + + expect(handler.canHandle(toolBlock)).toBe(false) + }) + + it.concurrent('should not handle blocks without trigger indicators', () => { + const regularBlock: SerializedBlock = { + id: 'regular-1', + metadata: { id: 'api', name: 'API Block', category: 'tools' }, + position: { x: 0, y: 0 }, + config: { tool: 'api', params: {} }, + inputs: {}, + outputs: {}, + enabled: true, + } + + expect(handler.canHandle(regularBlock)).toBe(false) + }) + + it.concurrent('should handle generic webhook blocks', () => { + const webhookBlock: SerializedBlock = { + id: 'webhook-1', + metadata: { id: 'generic_webhook', name: 'Generic Webhook', category: 'triggers' }, + position: { x: 0, y: 0 }, + config: { tool: 'generic_webhook', params: {} }, + inputs: {}, + outputs: {}, + enabled: true, + } + + expect(handler.canHandle(webhookBlock)).toBe(true) + }) + }) + + describe('execute', () => { + it.concurrent('should return inputs directly when provided', async () => { + const triggerBlock: SerializedBlock = { + id: 'trigger-1', + metadata: { id: 'gmail', name: 'Gmail Trigger', category: 'triggers' }, + position: { x: 0, y: 0 }, + config: { tool: 'gmail', params: {} }, + inputs: {}, + outputs: {}, + enabled: true, + } + + const triggerInputs = { + email: { + id: '12345', + subject: 'Test Email', + from: 'test@example.com', + body: 'Hello world', + }, + timestamp: '2023-01-01T12:00:00Z', + } + + const result = await handler.execute(triggerBlock, triggerInputs, mockContext) + + expect(result).toEqual(triggerInputs) + }) + + it.concurrent('should return empty object when no inputs provided', async () => { + const triggerBlock: SerializedBlock = { + id: 'trigger-1', + metadata: { id: 'schedule', name: 'Schedule Trigger', category: 'triggers' }, + position: { x: 0, y: 0 }, + config: { tool: 'schedule', params: {} }, + inputs: {}, + outputs: {}, + enabled: true, + } + + const result = await handler.execute(triggerBlock, {}, mockContext) + + expect(result).toEqual({}) + }) + + it.concurrent('should handle webhook payload inputs', async () => { + const webhookBlock: SerializedBlock = { + id: 'webhook-1', + metadata: { id: 'generic_webhook', name: 'Generic Webhook', category: 'triggers' }, + position: { x: 0, y: 0 }, + config: { tool: 'generic_webhook', params: {} }, + inputs: {}, + outputs: {}, + enabled: true, + } + + const webhookInputs = { + payload: { + event: 'user.created', + data: { + user: { + id: 'user123', + email: 'user@example.com', + }, + }, + }, + headers: { + 'content-type': 'application/json', + }, + method: 'POST', + } + + const result = await handler.execute(webhookBlock, webhookInputs, mockContext) + + expect(result).toEqual(webhookInputs) + }) + + it.concurrent('should handle Outlook trigger inputs', async () => { + const outlookBlock: SerializedBlock = { + id: 'outlook-1', + metadata: { id: 'outlook', name: 'Outlook Block', category: 'tools' }, + position: { x: 0, y: 0 }, + config: { tool: 'outlook', params: { triggerMode: true } }, + inputs: {}, + outputs: {}, + enabled: true, + } + + const outlookInputs = { + email: { + id: 'outlook123', + subject: 'Meeting Invitation', + from: 'colleague@company.com', + bodyPreview: 'Join us for the quarterly review...', + }, + timestamp: '2023-01-01T14:30:00Z', + } + + const result = await handler.execute(outlookBlock, outlookInputs, mockContext) + + expect(result).toEqual(outlookInputs) + }) + + it.concurrent('should handle schedule trigger with no inputs', async () => { + const scheduleBlock: SerializedBlock = { + id: 'schedule-1', + metadata: { id: 'schedule', name: 'Daily Schedule', category: 'triggers' }, + position: { x: 0, y: 0 }, + config: { tool: 'schedule', params: { scheduleType: 'daily' } }, + inputs: {}, + outputs: {}, + enabled: true, + } + + const result = await handler.execute(scheduleBlock, {}, mockContext) + + // Schedule triggers typically don't have input data, just trigger the workflow + expect(result).toEqual({}) + }) + + it.concurrent('should handle complex nested trigger data', async () => { + const triggerBlock: SerializedBlock = { + id: 'complex-trigger-1', + metadata: { id: 'webhook', name: 'Complex Webhook', category: 'triggers' }, + position: { x: 0, y: 0 }, + config: { tool: 'webhook', params: {} }, + inputs: {}, + outputs: {}, + enabled: true, + } + + const complexInputs = { + webhook: { + data: { + provider: 'github', + payload: { + action: 'opened', + pull_request: { + id: 123, + title: 'Fix bug in authentication', + user: { login: 'developer' }, + base: { ref: 'main' }, + head: { ref: 'fix-auth-bug' }, + }, + }, + headers: { 'x-github-event': 'pull_request' }, + }, + }, + timestamp: '2023-01-01T15:45:00Z', + } + + const result = await handler.execute(triggerBlock, complexInputs, mockContext) + + expect(result).toEqual(complexInputs) + }) + }) + + describe('integration scenarios', () => { + it.concurrent('should work with different trigger block types', () => { + const testCases = [ + { + name: 'Gmail in trigger mode', + block: { + id: 'gmail-trigger', + metadata: { id: 'gmail', category: 'tools' }, + config: { tool: 'gmail', params: { triggerMode: true } }, + }, + shouldHandle: true, + }, + { + name: 'Generic webhook', + block: { + id: 'webhook-trigger', + metadata: { id: 'generic_webhook', category: 'triggers' }, + config: { tool: 'generic_webhook', params: {} }, + }, + shouldHandle: true, + }, + { + name: 'Schedule block', + block: { + id: 'schedule-trigger', + metadata: { id: 'schedule', category: 'triggers' }, + config: { tool: 'schedule', params: {} }, + }, + shouldHandle: true, + }, + { + name: 'Regular API block', + block: { + id: 'api-block', + metadata: { id: 'api', category: 'tools' }, + config: { tool: 'api', params: {} }, + }, + shouldHandle: false, + }, + { + name: 'Gmail in tool mode', + block: { + id: 'gmail-tool', + metadata: { id: 'gmail', category: 'tools' }, + config: { tool: 'gmail', params: { triggerMode: false } }, + }, + shouldHandle: false, + }, + ] + + testCases.forEach(({ name, block, shouldHandle }) => { + const serializedBlock: SerializedBlock = { + ...block, + position: { x: 0, y: 0 }, + inputs: {}, + outputs: {}, + enabled: true, + } as SerializedBlock + + expect( + handler.canHandle(serializedBlock), + `${name} should ${shouldHandle ? '' : 'not '}be handled` + ).toBe(shouldHandle) + }) + }) + }) +}) diff --git a/apps/sim/executor/handlers/trigger/trigger-handler.ts b/apps/sim/executor/handlers/trigger/trigger-handler.ts new file mode 100644 index 0000000000..54b0638b06 --- /dev/null +++ b/apps/sim/executor/handlers/trigger/trigger-handler.ts @@ -0,0 +1,45 @@ +import { createLogger } from '@/lib/logs/console/logger' +import type { BlockHandler, ExecutionContext } from '@/executor/types' +import type { SerializedBlock } from '@/serializer/types' + +const logger = createLogger('TriggerBlockHandler') + +/** + * Handler for trigger blocks (Gmail, Webhook, Schedule, etc.) + * These blocks don't execute tools - they provide input data to workflows + */ +export class TriggerBlockHandler implements BlockHandler { + canHandle(block: SerializedBlock): boolean { + // Handle blocks that are triggers - either by category or by having triggerMode enabled + const isTriggerCategory = block.metadata?.category === 'triggers' + + // For blocks that can be both tools and triggers (like Gmail/Outlook), check if triggerMode is enabled + // This would come from the serialized block config/params + const hasTriggerMode = block.config?.params?.triggerMode === true + + return isTriggerCategory || hasTriggerMode + } + + async execute( + block: SerializedBlock, + inputs: Record, + _context: ExecutionContext + ): Promise { + logger.info(`Executing trigger block: ${block.id} (Type: ${block.metadata?.id})`) + + // Trigger blocks don't execute anything - they just pass through their input data + // The input data comes from the webhook execution context or initial workflow inputs + + // For trigger blocks, return the inputs directly - these contain the webhook/trigger data + if (inputs && Object.keys(inputs).length > 0) { + logger.debug(`Returning trigger inputs for block ${block.id}`, { + inputKeys: Object.keys(inputs), + }) + return inputs + } + + // Fallback - return empty object for trigger blocks with no inputs + logger.debug(`No inputs provided for trigger block ${block.id}, returning empty object`) + return {} + } +} diff --git a/apps/sim/executor/index.test.ts b/apps/sim/executor/index.test.ts index 7826c4b439..3d101ed333 100644 --- a/apps/sim/executor/index.test.ts +++ b/apps/sim/executor/index.test.ts @@ -8,6 +8,7 @@ * resolving inputs and dependencies, and managing errors. */ import { afterEach, beforeEach, describe, expect, vi } from 'vitest' +import type { BlockOutput, ParamType } from '@/blocks/types' import { Executor } from '@/executor' import { createMinimalWorkflow, @@ -187,6 +188,61 @@ describe('Executor', () => { ) }) + it.concurrent( + 'should NOT throw error if starter block has no outgoing connections but has trigger blocks', + () => { + const workflow = createMinimalWorkflow() + workflow.connections = [] + + // Add a trigger block (webhook trigger) + workflow.blocks.push({ + id: 'webhook-trigger', + position: { x: 0, y: 0 }, + metadata: { + category: 'triggers', + id: 'webhook', + }, + config: { + tool: 'webhook', + params: {}, + }, + inputs: {}, + outputs: {}, + enabled: true, + }) + + expect(() => new Executor(workflow)).not.toThrow() + } + ) + + it.concurrent( + 'should NOT throw error if starter block has no outgoing connections but has triggerMode block', + () => { + const workflow = createMinimalWorkflow() + workflow.connections = [] + + // Add a block with triggerMode enabled + workflow.blocks.push({ + id: 'gmail-trigger', + position: { x: 0, y: 0 }, + metadata: { + id: 'gmail', + }, + config: { + tool: 'gmail', + params: { + triggerMode: true, + }, + }, + inputs: {}, + outputs: {}, + enabled: true, + }) + + expect(() => new Executor(workflow)).not.toThrow() + } + ) + it.concurrent('should throw error if connection references non-existent source block', () => { const workflow = createMinimalWorkflow() workflow.connections.push({ @@ -974,29 +1030,33 @@ describe('Executor', () => { async () => { // Create a workflow with two parallel agents const workflow = { + version: '1.0', blocks: [ { id: 'starter', + position: { x: 0, y: 0 }, metadata: { id: BlockType.STARTER }, - subBlocks: {}, + config: { tool: 'starter', params: {} }, + inputs: {}, + outputs: {}, enabled: true, }, { id: 'agent1', + position: { x: 100, y: 0 }, metadata: { id: BlockType.AGENT, name: 'Agent 1' }, - subBlocks: { - model: { value: 'gpt-4o' }, - input: { value: 'Hello' }, - }, + config: { tool: 'agent', params: { model: 'gpt-4o', input: 'Hello' } }, + inputs: {}, + outputs: {}, enabled: true, }, { id: 'agent2', + position: { x: 200, y: 0 }, metadata: { id: BlockType.AGENT, name: 'Agent 2' }, - subBlocks: { - model: { value: 'gpt-4o' }, - input: { value: 'Hello' }, - }, + config: { tool: 'agent', params: { model: 'gpt-4o', input: 'Hello' } }, + inputs: {}, + outputs: {}, enabled: true, }, ], @@ -1004,8 +1064,8 @@ describe('Executor', () => { { source: 'starter', sourceHandle: 'out', target: 'agent1', targetHandle: 'in' }, { source: 'starter', sourceHandle: 'out', target: 'agent2', targetHandle: 'in' }, ], - loops: [], - parallels: [], + loops: {}, + parallels: {}, } const executor = new Executor(workflow) @@ -1054,4 +1114,55 @@ describe('Executor', () => { } ) }) + + /** + * Trigger handler integration tests + */ + describe('trigger block handling', () => { + it.concurrent('should not interfere with regular tool blocks', async () => { + const workflow = { + version: '1.0', + blocks: [ + { + id: 'starter', + position: { x: -100, y: 0 }, + metadata: { id: BlockType.STARTER, name: 'Starter Block' }, + config: { tool: 'starter', params: {} }, + inputs: {} as Record, + outputs: {} as Record, + enabled: true, + }, + { + id: 'api-block', + position: { x: 0, y: 0 }, + metadata: { id: BlockType.API, name: 'API Block', category: 'tools' }, + config: { tool: 'api', params: {} }, + inputs: { url: 'string' as ParamType }, + outputs: { response: 'json' as BlockOutput }, + enabled: true, + }, + ], + connections: [{ source: 'starter', target: 'api-block' }], + loops: {}, + } + + const executor = new Executor({ + workflow, + workflowInput: { url: 'https://api.example.com' }, + }) + + // The TriggerBlockHandler should NOT handle regular tool blocks + expect( + (executor as any).blockHandlers[0].canHandle({ + id: 'api-block', + metadata: { id: BlockType.API, category: 'tools' }, + config: { tool: 'api', params: {} }, + position: { x: 0, y: 0 }, + inputs: {}, + outputs: {}, + enabled: true, + }) + ).toBe(false) + }) + }) }) diff --git a/apps/sim/executor/index.ts b/apps/sim/executor/index.ts index b170d884bd..ab5cef2c54 100644 --- a/apps/sim/executor/index.ts +++ b/apps/sim/executor/index.ts @@ -13,6 +13,7 @@ import { ParallelBlockHandler, ResponseBlockHandler, RouterBlockHandler, + TriggerBlockHandler, WorkflowBlockHandler, } from '@/executor/handlers' import { LoopManager } from '@/executor/loops/loops' @@ -87,6 +88,7 @@ export class Executor { edges?: Array<{ source: string; target: string }> onStream?: (streamingExecution: StreamingExecution) => Promise executionId?: string + workspaceId?: string } }, private initialBlockStates: Record = {}, @@ -148,6 +150,7 @@ export class Executor { this.pathTracker = new PathTracker(this.actualWorkflow) this.blockHandlers = [ + new TriggerBlockHandler(), new AgentBlockHandler(), new RouterBlockHandler(this.pathTracker), new ConditionBlockHandler(this.pathTracker, this.resolver), @@ -618,12 +621,19 @@ export class Executor { throw new Error('Starter block cannot have incoming connections') } - // Only check outgoing connections for starter blocks, not trigger blocks - const outgoingFromStarter = this.actualWorkflow.connections.filter( - (conn) => conn.source === starterBlock.id - ) - if (outgoingFromStarter.length === 0) { - throw new Error('Starter block must have at least one outgoing connection') + // Check if there are any trigger blocks on the canvas + const hasTriggerBlocks = this.actualWorkflow.blocks.some((block) => { + return block.metadata?.category === 'triggers' || block.config?.params?.triggerMode === true + }) + + // Only check outgoing connections for starter blocks if there are no trigger blocks + if (!hasTriggerBlocks) { + const outgoingFromStarter = this.actualWorkflow.connections.filter( + (conn) => conn.source === starterBlock.id + ) + if (outgoingFromStarter.length === 0) { + throw new Error('Starter block must have at least one outgoing connection') + } } } @@ -675,6 +685,8 @@ export class Executor { ): ExecutionContext { const context: ExecutionContext = { workflowId, + workspaceId: this.contextExtensions.workspaceId, + executionId: this.contextExtensions.executionId, blockStates: new Map(), blockLogs: [], metadata: { @@ -797,6 +809,11 @@ export class Executor { ...finalInput, // Add input fields directly at top level } + // Add files if present (for all trigger types) + if (this.workflowInput?.files && Array.isArray(this.workflowInput.files)) { + blockOutput.files = this.workflowInput.files + } + logger.info(`[Executor] Starting block output:`, JSON.stringify(blockOutput, null, 2)) context.blockStates.set(initBlock.id, { @@ -804,6 +821,10 @@ export class Executor { executed: true, executionTime: 0, }) + + // Create a block log for the starter block if it has files + // This ensures files are captured in trace spans and execution logs + this.createStartedBlockWithFilesLog(initBlock, blockOutput, context) } else { // Handle structured input (like API calls or chat messages) if (this.workflowInput && typeof this.workflowInput === 'object') { @@ -812,17 +833,26 @@ export class Executor { Object.hasOwn(this.workflowInput, 'input') && Object.hasOwn(this.workflowInput, 'conversationId') ) { - // Chat workflow: extract input and conversationId to root level - const starterOutput = { + // Chat workflow: extract input, conversationId, and files to root level + const starterOutput: any = { input: this.workflowInput.input, conversationId: this.workflowInput.conversationId, } + // Add files if present + if (this.workflowInput.files && Array.isArray(this.workflowInput.files)) { + starterOutput.files = this.workflowInput.files + } + context.blockStates.set(initBlock.id, { output: starterOutput, executed: true, executionTime: 0, }) + + // Create a block log for the starter block if it has files + // This ensures files are captured in trace spans and execution logs + this.createStartedBlockWithFilesLog(initBlock, starterOutput, context) } else { // API workflow: spread the raw data directly (no wrapping) const starterOutput = { ...this.workflowInput } @@ -857,11 +887,16 @@ export class Executor { Object.hasOwn(this.workflowInput, 'input') && Object.hasOwn(this.workflowInput, 'conversationId') ) { - // Chat workflow: extract input and conversationId to root level + // Chat workflow: extract input, conversationId, and files to root level blockOutput = { input: this.workflowInput.input, conversationId: this.workflowInput.conversationId, } + + // Add files if present + if (this.workflowInput.files && Array.isArray(this.workflowInput.files)) { + blockOutput.files = this.workflowInput.files + } } else { // API workflow: spread the raw data directly (no wrapping) blockOutput = { ...this.workflowInput } @@ -883,6 +918,7 @@ export class Executor { executed: true, executionTime: 0, }) + this.createStartedBlockWithFilesLog(initBlock, blockOutput, context) } // Ensure the starting block is in the active execution path context.activeExecutionPath.add(initBlock.id) @@ -1806,4 +1842,29 @@ export class Executor { // Fallback to string conversion return String(error) } + + /** + * Creates a block log for the starter block if it contains files. + * This ensures files are captured in trace spans and execution logs. + */ + private createStartedBlockWithFilesLog( + initBlock: SerializedBlock, + blockOutput: any, + context: ExecutionContext + ): void { + if (blockOutput.files && Array.isArray(blockOutput.files) && blockOutput.files.length > 0) { + const starterBlockLog: BlockLog = { + blockId: initBlock.id, + blockName: initBlock.metadata?.name || 'Start', + blockType: initBlock.metadata?.id || 'start', + startedAt: new Date().toISOString(), + endedAt: new Date().toISOString(), + success: true, + input: this.workflowInput, + output: blockOutput, + durationMs: 0, + } + context.blockLogs.push(starterBlockLog) + } + } } diff --git a/apps/sim/executor/resolver/resolver.ts b/apps/sim/executor/resolver/resolver.ts index 13fbdd1210..6c82957b3d 100644 --- a/apps/sim/executor/resolver/resolver.ts +++ b/apps/sim/executor/resolver/resolver.ts @@ -8,6 +8,13 @@ import type { SerializedBlock, SerializedWorkflow } from '@/serializer/types' const logger = createLogger('InputResolver') +/** + * Helper function to resolve property access + */ +function resolvePropertyAccess(obj: any, property: string): any { + return obj[property] +} + /** * Resolves input values for blocks by handling references and variable substitution. */ @@ -516,7 +523,32 @@ export class InputResolver { throw new Error(`Invalid path "${part}" in "${path}" for starter block.`) } - replacementValue = replacementValue[part] + // Handle array indexing syntax like "files[0]" or "items[1]" + const arrayMatch = part.match(/^([^[]+)\[(\d+)\]$/) + if (arrayMatch) { + const [, arrayName, indexStr] = arrayMatch + const index = Number.parseInt(indexStr, 10) + + // First access the array property + const arrayValue = replacementValue[arrayName] + if (!Array.isArray(arrayValue)) { + throw new Error( + `Property "${arrayName}" is not an array in path "${path}" for starter block.` + ) + } + + // Then access the array element + if (index < 0 || index >= arrayValue.length) { + throw new Error( + `Array index ${index} is out of bounds for "${arrayName}" (length: ${arrayValue.length}) in path "${path}" for starter block.` + ) + } + + replacementValue = arrayValue[index] + } else { + // Regular property access with FileReference mapping + replacementValue = resolvePropertyAccess(replacementValue, part) + } if (replacementValue === undefined) { logger.warn( @@ -699,7 +731,32 @@ export class InputResolver { ) } - replacementValue = replacementValue[part] + // Handle array indexing syntax like "files[0]" or "items[1]" + const arrayMatch = part.match(/^([^[]+)\[(\d+)\]$/) + if (arrayMatch) { + const [, arrayName, indexStr] = arrayMatch + const index = Number.parseInt(indexStr, 10) + + // First access the array property + const arrayValue = replacementValue[arrayName] + if (!Array.isArray(arrayValue)) { + throw new Error( + `Property "${arrayName}" is not an array in path "${path}" for block "${sourceBlock.metadata?.name || sourceBlock.id}".` + ) + } + + // Then access the array element + if (index < 0 || index >= arrayValue.length) { + throw new Error( + `Array index ${index} is out of bounds for "${arrayName}" (length: ${arrayValue.length}) in path "${path}" for block "${sourceBlock.metadata?.name || sourceBlock.id}".` + ) + } + + replacementValue = arrayValue[index] + } else { + // Regular property access with FileReference mapping + replacementValue = resolvePropertyAccess(replacementValue, part) + } if (replacementValue === undefined) { throw new Error( diff --git a/apps/sim/executor/types.ts b/apps/sim/executor/types.ts index d8c078c4b5..8168ca9ec6 100644 --- a/apps/sim/executor/types.ts +++ b/apps/sim/executor/types.ts @@ -1,6 +1,20 @@ import type { BlockOutput } from '@/blocks/types' import type { SerializedBlock, SerializedWorkflow } from '@/serializer/types' +/** + * User-facing file object with simplified interface + */ +export interface UserFile { + id: string + name: string + url: string + size: number + type: string + key: string + uploadedAt: string + expiresAt: string +} + /** * Standardized block output format that ensures compatibility with the execution engine. */ @@ -18,6 +32,8 @@ export interface NormalizedBlockOutput { list: any[] count: number } + // File fields + files?: UserFile[] // Binary files/attachments from this block // Path selection fields selectedPath?: { blockId: string @@ -81,6 +97,8 @@ export interface BlockState { */ export interface ExecutionContext { workflowId: string // Unique identifier for this workflow execution + workspaceId?: string // Workspace ID for file storage scoping + executionId?: string // Unique execution ID for file storage scoping blockStates: Map blockLogs: BlockLog[] // Chronological log of block executions metadata: ExecutionMetadata // Timing metadata for the execution diff --git a/apps/sim/executor/utils/file-tool-processor.ts b/apps/sim/executor/utils/file-tool-processor.ts new file mode 100644 index 0000000000..b6d4d2735d --- /dev/null +++ b/apps/sim/executor/utils/file-tool-processor.ts @@ -0,0 +1,200 @@ +import { createLogger } from '@/lib/logs/console/logger' +import { uploadExecutionFile } from '@/lib/workflows/execution-file-storage' +import type { ExecutionContext, UserFile } from '@/executor/types' +import type { ToolConfig, ToolFileData } from '@/tools/types' + +const logger = createLogger('FileToolProcessor') + +/** + * Processes tool outputs and converts file-typed outputs to UserFile objects. + * This enables tools to return file data that gets automatically stored in the + * execution filesystem and made available as UserFile objects for workflow use. + */ +export class FileToolProcessor { + /** + * Process tool outputs and convert file-typed outputs to UserFile objects + */ + static async processToolOutputs( + toolOutput: any, + toolConfig: ToolConfig, + executionContext: ExecutionContext + ): Promise { + if (!toolConfig.outputs) { + return toolOutput + } + + const processedOutput = { ...toolOutput } + + // Process each output that's marked as file or file[] + for (const [outputKey, outputDef] of Object.entries(toolConfig.outputs)) { + if (!FileToolProcessor.isFileOutput(outputDef.type)) { + continue + } + + const fileData = processedOutput[outputKey] + if (!fileData) { + logger.warn(`File-typed output '${outputKey}' is missing from tool result`) + continue + } + + try { + processedOutput[outputKey] = await FileToolProcessor.processFileOutput( + fileData, + outputDef.type, + outputKey, + executionContext + ) + } catch (error) { + logger.error(`Error processing file output '${outputKey}':`, error) + const errorMessage = error instanceof Error ? error.message : String(error) + throw new Error(`Failed to process file output '${outputKey}': ${errorMessage}`) + } + } + + return processedOutput + } + + /** + * Check if an output type is file-related + */ + private static isFileOutput(type: string): boolean { + return type === 'file' || type === 'file[]' + } + + /** + * Process a single file output (either single file or array of files) + */ + private static async processFileOutput( + fileData: any, + outputType: string, + outputKey: string, + executionContext: ExecutionContext + ): Promise { + if (outputType === 'file[]') { + return FileToolProcessor.processFileArray(fileData, outputKey, executionContext) + } + return FileToolProcessor.processFileData(fileData, executionContext, outputKey) + } + + /** + * Process an array of files + */ + private static async processFileArray( + fileData: any, + outputKey: string, + executionContext: ExecutionContext + ): Promise { + if (!Array.isArray(fileData)) { + throw new Error(`Output '${outputKey}' is marked as file[] but is not an array`) + } + + return Promise.all( + fileData.map((file, index) => + FileToolProcessor.processFileData(file, executionContext, `${outputKey}[${index}]`) + ) + ) + } + + /** + * Convert various file data formats to UserFile by storing in execution filesystem + */ + private static async processFileData( + fileData: ToolFileData, + context: ExecutionContext, + outputKey: string + ): Promise { + logger.info(`Processing file data for output '${outputKey}': ${fileData.name}`) + try { + // Convert various formats to Buffer + let buffer: Buffer + + if (Buffer.isBuffer(fileData.data)) { + buffer = fileData.data + logger.info(`Using Buffer data for ${fileData.name} (${buffer.length} bytes)`) + } else if ( + fileData.data && + typeof fileData.data === 'object' && + 'type' in fileData.data && + 'data' in fileData.data + ) { + // Handle serialized Buffer objects (from JSON serialization) + const serializedBuffer = fileData.data as { type: string; data: number[] } + if (serializedBuffer.type === 'Buffer' && Array.isArray(serializedBuffer.data)) { + buffer = Buffer.from(serializedBuffer.data) + } else { + throw new Error(`Invalid serialized buffer format for ${fileData.name}`) + } + logger.info( + `Converted serialized Buffer to Buffer for ${fileData.name} (${buffer.length} bytes)` + ) + } else if (typeof fileData.data === 'string' && fileData.data) { + // Assume base64 or base64url + let base64Data = fileData.data + + // Convert base64url to base64 if needed (Gmail API format) + if (base64Data && (base64Data.includes('-') || base64Data.includes('_'))) { + base64Data = base64Data.replace(/-/g, '+').replace(/_/g, '/') + } + + buffer = Buffer.from(base64Data, 'base64') + logger.info( + `Converted base64 string to Buffer for ${fileData.name} (${buffer.length} bytes)` + ) + } else if (fileData.url) { + // Download from URL + logger.info(`Downloading file from URL: ${fileData.url}`) + const response = await fetch(fileData.url) + + if (!response.ok) { + throw new Error(`Failed to download file from ${fileData.url}: ${response.statusText}`) + } + + const arrayBuffer = await response.arrayBuffer() + buffer = Buffer.from(arrayBuffer) + logger.info(`Downloaded file from URL for ${fileData.name} (${buffer.length} bytes)`) + } else { + throw new Error( + `File data for '${fileData.name}' must have either 'data' (Buffer/base64) or 'url' property` + ) + } + + // Validate buffer + if (buffer.length === 0) { + throw new Error(`File '${fileData.name}' has zero bytes`) + } + + // Store in execution filesystem + const userFile = await uploadExecutionFile( + { + workspaceId: context.workspaceId || '', + workflowId: context.workflowId, + executionId: context.executionId || '', + }, + buffer, + fileData.name, + fileData.mimeType + ) + + logger.info( + `Successfully stored file '${fileData.name}' in execution filesystem with key: ${userFile.key}` + ) + return userFile + } catch (error) { + logger.error(`Error processing file data for '${fileData.name}':`, error) + throw error + } + } + + /** + * Check if a tool has any file-typed outputs + */ + static hasFileOutputs(toolConfig: ToolConfig): boolean { + if (!toolConfig.outputs) { + return false + } + + return Object.values(toolConfig.outputs).some( + (output) => output.type === 'file' || output.type === 'file[]' + ) + } +} diff --git a/apps/sim/hooks/use-collaborative-workflow.ts b/apps/sim/hooks/use-collaborative-workflow.ts index 5bf53e7fd1..356221c440 100644 --- a/apps/sim/hooks/use-collaborative-workflow.ts +++ b/apps/sim/hooks/use-collaborative-workflow.ts @@ -98,7 +98,15 @@ export function useCollaborativeWorkflow() { payload.position, payload.data, payload.parentId, - payload.extent + payload.extent, + { + enabled: payload.enabled, + horizontalHandles: payload.horizontalHandles, + isWide: payload.isWide, + advancedMode: payload.advancedMode, + triggerMode: payload.triggerMode ?? false, + height: payload.height, + } ) if (payload.autoConnectEdge) { workflowStore.addEdge(payload.autoConnectEdge) @@ -152,6 +160,9 @@ export function useCollaborativeWorkflow() { case 'update-advanced-mode': workflowStore.setBlockAdvancedMode(payload.id, payload.advancedMode) break + case 'update-trigger-mode': + workflowStore.setBlockTriggerMode(payload.id, payload.triggerMode) + break case 'toggle-handles': { const currentBlock = workflowStore.blocks[payload.id] if (currentBlock && currentBlock.horizontalHandles !== payload.horizontalHandles) { @@ -167,7 +178,15 @@ export function useCollaborativeWorkflow() { payload.position, payload.data, payload.parentId, - payload.extent + payload.extent, + { + enabled: payload.enabled, + horizontalHandles: payload.horizontalHandles, + isWide: payload.isWide, + advancedMode: payload.advancedMode, + triggerMode: payload.triggerMode ?? false, + height: payload.height, + } ) // Handle auto-connect edge if present if (payload.autoConnectEdge) { @@ -462,7 +481,9 @@ export function useCollaborativeWorkflow() { // Skip if applying remote changes if (isApplyingRemoteChange.current) { - workflowStore.addBlock(id, type, name, position, data, parentId, extent) + workflowStore.addBlock(id, type, name, position, data, parentId, extent, { + triggerMode: false, + }) if (autoConnectEdge) { workflowStore.addEdge(autoConnectEdge) } @@ -485,7 +506,9 @@ export function useCollaborativeWorkflow() { }) // Apply locally first (immediate UI feedback) - workflowStore.addBlock(id, type, name, position, data, parentId, extent) + workflowStore.addBlock(id, type, name, position, data, parentId, extent, { + triggerMode: false, + }) if (autoConnectEdge) { workflowStore.addEdge(autoConnectEdge) } @@ -526,6 +549,7 @@ export function useCollaborativeWorkflow() { horizontalHandles: true, isWide: false, advancedMode: false, + triggerMode: false, height: 0, // Default height, will be set by the UI parentId, extent, @@ -551,7 +575,9 @@ export function useCollaborativeWorkflow() { }) // Apply locally - workflowStore.addBlock(id, type, name, position, data, parentId, extent) + workflowStore.addBlock(id, type, name, position, data, parentId, extent, { + triggerMode: false, + }) if (autoConnectEdge) { workflowStore.addEdge(autoConnectEdge) } @@ -670,6 +696,23 @@ export function useCollaborativeWorkflow() { [executeQueuedOperation, workflowStore] ) + const collaborativeToggleBlockTriggerMode = useCallback( + (id: string) => { + const currentBlock = workflowStore.blocks[id] + if (!currentBlock) return + + const newTriggerMode = !currentBlock.triggerMode + + executeQueuedOperation( + 'update-trigger-mode', + 'block', + { id, triggerMode: newTriggerMode }, + () => workflowStore.toggleBlockTriggerMode(id) + ) + }, + [executeQueuedOperation, workflowStore] + ) + const collaborativeToggleBlockHandles = useCallback( (id: string) => { const currentBlock = workflowStore.blocks[id] @@ -841,6 +884,7 @@ export function useCollaborativeWorkflow() { horizontalHandles: sourceBlock.horizontalHandles ?? true, isWide: sourceBlock.isWide ?? false, advancedMode: sourceBlock.advancedMode ?? false, + triggerMode: false, // Always duplicate as normal mode to avoid webhook conflicts height: sourceBlock.height || 0, } @@ -857,6 +901,7 @@ export function useCollaborativeWorkflow() { horizontalHandles: sourceBlock.horizontalHandles, isWide: sourceBlock.isWide, advancedMode: sourceBlock.advancedMode, + triggerMode: false, // Always duplicate as normal mode height: sourceBlock.height, } ) @@ -875,6 +920,7 @@ export function useCollaborativeWorkflow() { horizontalHandles: sourceBlock.horizontalHandles, isWide: sourceBlock.isWide, advancedMode: sourceBlock.advancedMode, + triggerMode: false, // Always duplicate as normal mode height: sourceBlock.height, } ) @@ -1096,6 +1142,7 @@ export function useCollaborativeWorkflow() { collaborativeUpdateParentId, collaborativeToggleBlockWide, collaborativeToggleBlockAdvancedMode, + collaborativeToggleBlockTriggerMode, collaborativeToggleBlockHandles, collaborativeDuplicateBlock, collaborativeAddEdge, diff --git a/apps/sim/lib/auth.ts b/apps/sim/lib/auth.ts index 4d1b0127e5..e3ef7354dc 100644 --- a/apps/sim/lib/auth.ts +++ b/apps/sim/lib/auth.ts @@ -21,6 +21,7 @@ import { } from '@/components/emails/render-email' import { getBaseURL } from '@/lib/auth-client' import { DEFAULT_FREE_CREDITS } from '@/lib/billing/constants' +import { quickValidateEmail } from '@/lib/email/validation' import { env, isTruthy } from '@/lib/env' import { isProd } from '@/lib/environment' import { createLogger } from '@/lib/logs/console/logger' @@ -165,7 +166,7 @@ export const auth = betterAuth({ const html = await renderPasswordResetEmail(username, url) const result = await resend.emails.send({ - from: `Sim `, + from: `Sim `, to: user.email, subject: getEmailSubject('reset-password'), html, @@ -181,6 +182,39 @@ export const auth = betterAuth({ if (ctx.path.startsWith('/sign-up') && isTruthy(env.DISABLE_REGISTRATION)) throw new Error('Registration is disabled, please contact your admin.') + // Check email and domain whitelist for sign-in and sign-up + if ( + (ctx.path.startsWith('/sign-in') || ctx.path.startsWith('/sign-up')) && + (env.ALLOWED_LOGIN_EMAILS || env.ALLOWED_LOGIN_DOMAINS) + ) { + const requestEmail = ctx.body?.email?.toLowerCase() + + if (requestEmail) { + let isAllowed = false + + // Check specific email whitelist + if (env.ALLOWED_LOGIN_EMAILS) { + const allowedEmails = env.ALLOWED_LOGIN_EMAILS.split(',').map((email) => + email.trim().toLowerCase() + ) + isAllowed = allowedEmails.includes(requestEmail) + } + + // Check domain whitelist if not already allowed + if (!isAllowed && env.ALLOWED_LOGIN_DOMAINS) { + const allowedDomains = env.ALLOWED_LOGIN_DOMAINS.split(',').map((domain) => + domain.trim().toLowerCase() + ) + const emailDomain = requestEmail.split('@')[1] + isAllowed = emailDomain && allowedDomains.includes(emailDomain) + } + + if (!isAllowed) { + throw new Error('Access restricted. Please contact your administrator.') + } + } + } + return }), }, @@ -204,12 +238,27 @@ export const auth = betterAuth({ throw new Error('Email is required') } + // Validate email before sending OTP + const validation = quickValidateEmail(data.email) + if (!validation.isValid) { + logger.warn('Email validation failed', { + email: data.email, + reason: validation.reason, + checks: validation.checks, + }) + throw new Error( + validation.reason || + "We are unable to deliver the verification email to that address. Please make sure it's valid and able to receive emails." + ) + } + // In development with no RESEND_API_KEY, log verification code if (!validResendAPIKEY) { logger.info('🔑 VERIFICATION CODE FOR LOGIN/SIGNUP', { email: data.email, otp: data.otp, type: data.type, + validation: validation.checks, }) return } @@ -218,7 +267,7 @@ export const auth = betterAuth({ // In production, send an actual email const result = await resend.emails.send({ - from: `Sim `, + from: `Sim `, to: data.email, subject: getEmailSubject(data.type), html, @@ -1479,7 +1528,7 @@ export const auth = betterAuth({ ) await resend.emails.send({ - from: `Sim `, + from: `Sim `, to: invitation.email, subject: `${inviterName} has invited you to join ${organization.name} on Sim`, html, diff --git a/apps/sim/lib/billing/validation/seat-management.ts b/apps/sim/lib/billing/validation/seat-management.ts index 5516fd45e1..988bb59df1 100644 --- a/apps/sim/lib/billing/validation/seat-management.ts +++ b/apps/sim/lib/billing/validation/seat-management.ts @@ -1,5 +1,6 @@ import { and, count, eq } from 'drizzle-orm' import { getHighestPrioritySubscription } from '@/lib/billing/core/subscription' +import { quickValidateEmail } from '@/lib/email/validation' import { createLogger } from '@/lib/logs/console/logger' import { db } from '@/db' import { invitation, member, organization, subscription, user, userStats } from '@/db/schema' @@ -208,8 +209,9 @@ export async function validateBulkInvitations( try { // Remove duplicates and validate email format const uniqueEmails = [...new Set(emailList)] - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ - const validEmails = uniqueEmails.filter((email) => emailRegex.test(email)) + const validEmails = uniqueEmails.filter( + (email) => quickValidateEmail(email.trim().toLowerCase()).isValid + ) const duplicateEmails = emailList.filter((email, index) => emailList.indexOf(email) !== index) // Check for existing members diff --git a/apps/sim/lib/documents/chunker.ts b/apps/sim/lib/documents/chunker.ts index e93b2f1f8e..d24680cc34 100644 --- a/apps/sim/lib/documents/chunker.ts +++ b/apps/sim/lib/documents/chunker.ts @@ -57,7 +57,7 @@ export class TextChunker { constructor(options: ChunkerOptions = {}) { this.chunkSize = options.chunkSize ?? 512 - this.minChunkSize = options.minChunkSize ?? 50 + this.minChunkSize = options.minChunkSize ?? 1 this.overlap = options.overlap ?? 0 } diff --git a/apps/sim/lib/documents/docs-chunker.ts b/apps/sim/lib/documents/docs-chunker.ts index feaf40efb8..192124109d 100644 --- a/apps/sim/lib/documents/docs-chunker.ts +++ b/apps/sim/lib/documents/docs-chunker.ts @@ -25,7 +25,7 @@ export class DocsChunker { // Use the existing TextChunker for chunking logic this.textChunker = new TextChunker({ chunkSize: options.chunkSize ?? 300, // Max 300 tokens per chunk - minChunkSize: options.minChunkSize ?? 100, + minChunkSize: options.minChunkSize ?? 1, overlap: options.overlap ?? 50, }) // Use localhost docs in development, production docs otherwise diff --git a/apps/sim/lib/documents/document-processor.ts b/apps/sim/lib/documents/document-processor.ts index 1b7639ba4c..add31c177f 100644 --- a/apps/sim/lib/documents/document-processor.ts +++ b/apps/sim/lib/documents/document-processor.ts @@ -61,7 +61,8 @@ export async function processDocument( filename: string, mimeType: string, chunkSize = 1000, - chunkOverlap = 200 + chunkOverlap = 200, + minChunkSize = 1 ): Promise<{ chunks: Chunk[] metadata: { @@ -85,6 +86,7 @@ export async function processDocument( const chunker = new TextChunker({ chunkSize, overlap: chunkOverlap, + minChunkSize, }) const chunks = await chunker.chunk(content) diff --git a/apps/sim/lib/email/mailer.ts b/apps/sim/lib/email/mailer.ts index 772faf35d6..25aa227346 100644 --- a/apps/sim/lib/email/mailer.ts +++ b/apps/sim/lib/email/mailer.ts @@ -68,7 +68,7 @@ export async function sendEmail({ } } - const senderEmail = from || `noreply@${getEmailDomain()}` + const senderEmail = from || `noreply@${env.EMAIL_DOMAIN || getEmailDomain()}` if (!resend) { logger.info('Email not sent (Resend not configured):', { @@ -132,7 +132,7 @@ export async function sendBatchEmails({ emails, }: BatchEmailOptions): Promise { try { - const senderEmail = `noreply@${getEmailDomain()}` + const senderEmail = `noreply@${env.EMAIL_DOMAIN || getEmailDomain()}` const results: SendEmailResult[] = [] if (!resend) { diff --git a/apps/sim/lib/email/utils.test.ts b/apps/sim/lib/email/utils.test.ts deleted file mode 100644 index c8c44d9e53..0000000000 --- a/apps/sim/lib/email/utils.test.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { validateAndNormalizeEmail } from '@/lib/email/utils' - -describe('validateAndNormalizeEmail', () => { - describe('valid emails', () => { - it.concurrent('should validate simple email addresses', () => { - const result = validateAndNormalizeEmail('test@example.com') - expect(result.isValid).toBe(true) - expect(result.normalized).toBe('test@example.com') - }) - - it.concurrent('should validate emails with subdomains', () => { - const result = validateAndNormalizeEmail('user@mail.example.com') - expect(result.isValid).toBe(true) - expect(result.normalized).toBe('user@mail.example.com') - }) - - it.concurrent('should validate emails with numbers and hyphens', () => { - const result = validateAndNormalizeEmail('user123@test-domain.co.uk') - expect(result.isValid).toBe(true) - expect(result.normalized).toBe('user123@test-domain.co.uk') - }) - - it.concurrent('should validate emails with special characters in local part', () => { - const result = validateAndNormalizeEmail('user.name+tag@example.com') - expect(result.isValid).toBe(true) - expect(result.normalized).toBe('user.name+tag@example.com') - }) - }) - - describe('invalid emails', () => { - it.concurrent('should reject emails without @ symbol', () => { - const result = validateAndNormalizeEmail('testexample.com') - expect(result.isValid).toBe(false) - expect(result.normalized).toBe('testexample.com') - }) - - it.concurrent('should reject emails without domain', () => { - const result = validateAndNormalizeEmail('test@') - expect(result.isValid).toBe(false) - expect(result.normalized).toBe('test@') - }) - - it.concurrent('should reject emails without local part', () => { - const result = validateAndNormalizeEmail('@example.com') - expect(result.isValid).toBe(false) - expect(result.normalized).toBe('@example.com') - }) - - it.concurrent('should reject emails without TLD', () => { - const result = validateAndNormalizeEmail('test@domain') - expect(result.isValid).toBe(false) - expect(result.normalized).toBe('test@domain') - }) - - it.concurrent('should reject empty strings', () => { - const result = validateAndNormalizeEmail('') - expect(result.isValid).toBe(false) - expect(result.normalized).toBe('') - }) - - it.concurrent('should reject emails with spaces', () => { - const result = validateAndNormalizeEmail('test @example.com') - expect(result.isValid).toBe(false) - expect(result.normalized).toBe('test @example.com') - }) - - it.concurrent('should reject emails with multiple @ symbols', () => { - const result = validateAndNormalizeEmail('test@@example.com') - expect(result.isValid).toBe(false) - expect(result.normalized).toBe('test@@example.com') - }) - }) - - describe('normalization', () => { - it.concurrent('should trim whitespace from email', () => { - const result = validateAndNormalizeEmail(' test@example.com ') - expect(result.isValid).toBe(true) - expect(result.normalized).toBe('test@example.com') - }) - - it.concurrent('should convert email to lowercase', () => { - const result = validateAndNormalizeEmail('Test.User@EXAMPLE.COM') - expect(result.isValid).toBe(true) - expect(result.normalized).toBe('test.user@example.com') - }) - - it.concurrent('should trim and convert to lowercase together', () => { - const result = validateAndNormalizeEmail(' Test.User@EXAMPLE.COM ') - expect(result.isValid).toBe(true) - expect(result.normalized).toBe('test.user@example.com') - }) - - it.concurrent('should normalize invalid emails as well', () => { - const result = validateAndNormalizeEmail(' INVALID EMAIL ') - expect(result.isValid).toBe(false) - expect(result.normalized).toBe('invalid email') - }) - }) - - describe('edge cases', () => { - it.concurrent('should handle only whitespace', () => { - const result = validateAndNormalizeEmail(' ') - expect(result.isValid).toBe(false) - expect(result.normalized).toBe('') - }) - - it.concurrent('should handle tab and newline characters', () => { - const result = validateAndNormalizeEmail('\t\ntest@example.com\t\n') - expect(result.isValid).toBe(true) - expect(result.normalized).toBe('test@example.com') - }) - }) -}) diff --git a/apps/sim/lib/email/utils.ts b/apps/sim/lib/email/utils.ts deleted file mode 100644 index 8816f688b0..0000000000 --- a/apps/sim/lib/email/utils.ts +++ /dev/null @@ -1,10 +0,0 @@ -export const validateAndNormalizeEmail = ( - email: string -): { isValid: boolean; normalized: string } => { - const normalized = email.trim().toLowerCase() - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ - return { - isValid: emailRegex.test(normalized), - normalized, - } -} diff --git a/apps/sim/lib/email/validation.test.ts b/apps/sim/lib/email/validation.test.ts new file mode 100644 index 0000000000..e179abb83d --- /dev/null +++ b/apps/sim/lib/email/validation.test.ts @@ -0,0 +1,75 @@ +import { quickValidateEmail, validateEmail } from './validation' + +describe('Email Validation', () => { + describe('validateEmail', () => { + it.concurrent('should validate a correct email', async () => { + const result = await validateEmail('user@example.com') + expect(result.isValid).toBe(true) + expect(result.checks.syntax).toBe(true) + expect(result.checks.disposable).toBe(true) + }) + + it.concurrent('should reject invalid syntax', async () => { + const result = await validateEmail('invalid-email') + expect(result.isValid).toBe(false) + expect(result.reason).toBe('Invalid email format') + expect(result.checks.syntax).toBe(false) + }) + + it.concurrent('should reject disposable email addresses', async () => { + const result = await validateEmail('test@10minutemail.com') + expect(result.isValid).toBe(false) + expect(result.reason).toBe('Disposable email addresses are not allowed') + expect(result.checks.disposable).toBe(false) + }) + + it.concurrent('should accept legitimate business emails', async () => { + const legitimateEmails = [ + 'test@gmail.com', + 'noreply@gmail.com', + 'no-reply@yahoo.com', + 'user12345@outlook.com', + 'longusernamehere@gmail.com', + ] + + for (const email of legitimateEmails) { + const result = await validateEmail(email) + expect(result.isValid).toBe(true) + } + }) + + it.concurrent('should reject consecutive dots (RFC violation)', async () => { + const result = await validateEmail('user..name@example.com') + expect(result.isValid).toBe(false) + expect(result.reason).toBe('Email contains suspicious patterns') + }) + + it.concurrent('should reject very long local parts (RFC violation)', async () => { + const longLocalPart = 'a'.repeat(65) + const result = await validateEmail(`${longLocalPart}@example.com`) + expect(result.isValid).toBe(false) + expect(result.reason).toBe('Email contains suspicious patterns') + }) + }) + + describe('quickValidateEmail', () => { + it.concurrent('should validate quickly without MX check', () => { + const result = quickValidateEmail('user@example.com') + expect(result.isValid).toBe(true) + expect(result.checks.mxRecord).toBe(true) // Skipped, so assumed true + expect(result.confidence).toBe('medium') + }) + + it.concurrent('should reject invalid emails quickly', () => { + const result = quickValidateEmail('invalid-email') + expect(result.isValid).toBe(false) + expect(result.reason).toBe('Invalid email format') + }) + + it.concurrent('should reject disposable emails quickly', () => { + const result = quickValidateEmail('test@tempmail.org') + expect(result.isValid).toBe(false) + expect(result.reason).toBe('Disposable email addresses are not allowed') + }) + }) +}) diff --git a/apps/sim/lib/email/validation.ts b/apps/sim/lib/email/validation.ts new file mode 100644 index 0000000000..a9e5bc5f0d --- /dev/null +++ b/apps/sim/lib/email/validation.ts @@ -0,0 +1,262 @@ +import { createLogger } from '@/lib/logs/console/logger' + +const logger = createLogger('EmailValidation') + +export interface EmailValidationResult { + isValid: boolean + reason?: string + confidence: 'high' | 'medium' | 'low' + checks: { + syntax: boolean + domain: boolean + mxRecord: boolean + disposable: boolean + } +} + +// Common disposable email domains (subset - can be expanded) +const DISPOSABLE_DOMAINS = new Set([ + '10minutemail.com', + 'tempmail.org', + 'guerrillamail.com', + 'mailinator.com', + 'yopmail.com', + 'temp-mail.org', + 'throwaway.email', + 'getnada.com', + '10minutemail.net', + 'temporary-mail.net', + 'fakemailgenerator.com', + 'sharklasers.com', + 'guerrillamailblock.com', + 'pokemail.net', + 'spam4.me', + 'tempail.com', + 'tempr.email', + 'dispostable.com', + 'emailondeck.com', +]) + +/** + * Validates email syntax using RFC 5322 compliant regex + */ +function validateEmailSyntax(email: string): boolean { + const emailRegex = + /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/ + return emailRegex.test(email) && email.length <= 254 +} + +/** + * Checks if domain has valid MX records (server-side only) + */ +async function checkMXRecord(domain: string): Promise { + // Skip MX check on client-side (browser) + if (typeof window !== 'undefined') { + return true // Assume valid on client-side + } + + try { + const { promisify } = await import('util') + const dns = await import('dns') + const resolveMx = promisify(dns.resolveMx) + + const mxRecords = await resolveMx(domain) + return mxRecords && mxRecords.length > 0 + } catch (error) { + logger.debug('MX record check failed', { domain, error }) + return false + } +} + +/** + * Checks if email is from a known disposable email provider + */ +function isDisposableEmail(email: string): boolean { + const domain = email.split('@')[1]?.toLowerCase() + return domain ? DISPOSABLE_DOMAINS.has(domain) : false +} + +/** + * Checks for obvious patterns that indicate invalid emails + */ +function hasInvalidPatterns(email: string): boolean { + // Check for consecutive dots (RFC violation) + if (email.includes('..')) return true + + // Check for local part length (RFC limit is 64 characters) + const localPart = email.split('@')[0] + if (localPart && localPart.length > 64) return true + + return false +} + +/** + * Validates an email address comprehensively + */ +export async function validateEmail(email: string): Promise { + const checks = { + syntax: false, + domain: false, + mxRecord: false, + disposable: false, + } + + try { + // 1. Basic syntax validation + checks.syntax = validateEmailSyntax(email) + if (!checks.syntax) { + return { + isValid: false, + reason: 'Invalid email format', + confidence: 'high', + checks, + } + } + + const domain = email.split('@')[1]?.toLowerCase() + if (!domain) { + return { + isValid: false, + reason: 'Missing domain', + confidence: 'high', + checks, + } + } + + // 2. Check for disposable email first (more specific) + checks.disposable = !isDisposableEmail(email) + if (!checks.disposable) { + return { + isValid: false, + reason: 'Disposable email addresses are not allowed', + confidence: 'high', + checks, + } + } + + // 3. Check for invalid patterns + if (hasInvalidPatterns(email)) { + return { + isValid: false, + reason: 'Email contains suspicious patterns', + confidence: 'high', + checks, + } + } + + // 4. Domain validation - check for obvious invalid domains + checks.domain = domain.includes('.') && !domain.startsWith('.') && !domain.endsWith('.') + if (!checks.domain) { + return { + isValid: false, + reason: 'Invalid domain format', + confidence: 'high', + checks, + } + } + + // 5. MX record check (with timeout) + try { + const mxCheckPromise = checkMXRecord(domain) + const timeoutPromise = new Promise((_, reject) => + setTimeout(() => reject(new Error('MX check timeout')), 5000) + ) + + checks.mxRecord = await Promise.race([mxCheckPromise, timeoutPromise]) + } catch (error) { + logger.debug('MX record check failed or timed out', { domain, error }) + checks.mxRecord = false + } + + // Determine overall validity and confidence + if (!checks.mxRecord) { + return { + isValid: false, + reason: 'Domain does not accept emails (no MX records)', + confidence: 'high', + checks, + } + } + + return { + isValid: true, + confidence: 'high', + checks, + } + } catch (error) { + logger.error('Email validation error', { email, error }) + return { + isValid: false, + reason: 'Validation service temporarily unavailable', + confidence: 'low', + checks, + } + } +} + +/** + * Quick validation for high-volume scenarios (skips MX check) + */ +export function quickValidateEmail(email: string): EmailValidationResult { + const checks = { + syntax: false, + domain: false, + mxRecord: true, // Skip MX check for performance + disposable: false, + } + + checks.syntax = validateEmailSyntax(email) + if (!checks.syntax) { + return { + isValid: false, + reason: 'Invalid email format', + confidence: 'high', + checks, + } + } + + const domain = email.split('@')[1]?.toLowerCase() + if (!domain) { + return { + isValid: false, + reason: 'Missing domain', + confidence: 'high', + checks, + } + } + + checks.disposable = !isDisposableEmail(email) + if (!checks.disposable) { + return { + isValid: false, + reason: 'Disposable email addresses are not allowed', + confidence: 'high', + checks, + } + } + + if (hasInvalidPatterns(email)) { + return { + isValid: false, + reason: 'Email contains suspicious patterns', + confidence: 'medium', + checks, + } + } + + checks.domain = domain.includes('.') && !domain.startsWith('.') && !domain.endsWith('.') + if (!checks.domain) { + return { + isValid: false, + reason: 'Invalid domain format', + confidence: 'high', + checks, + } + } + + return { + isValid: true, + confidence: 'medium', + checks, + } +} diff --git a/apps/sim/lib/env.ts b/apps/sim/lib/env.ts index 44b398a88f..223e7f2f0b 100644 --- a/apps/sim/lib/env.ts +++ b/apps/sim/lib/env.ts @@ -20,6 +20,8 @@ export const env = createEnv({ BETTER_AUTH_URL: z.string().url(), // Base URL for Better Auth service BETTER_AUTH_SECRET: z.string().min(32), // Secret key for Better Auth JWT signing DISABLE_REGISTRATION: z.boolean().optional(), // Flag to disable new user registration + ALLOWED_LOGIN_EMAILS: z.string().optional(), // Comma-separated list of allowed email addresses for login + ALLOWED_LOGIN_DOMAINS: z.string().optional(), // Comma-separated list of allowed email domains for login ENCRYPTION_KEY: z.string().min(32), // Key for encrypting sensitive data INTERNAL_API_SECRET: z.string().min(32), // Secret for internal API authentication SIM_AGENT_API_KEY: z.string().min(1).optional(), // Secret for internal sim agent API authentication @@ -95,6 +97,7 @@ export const env = createEnv({ S3_BUCKET_NAME: z.string().optional(), // S3 bucket for general file storage S3_LOGS_BUCKET_NAME: z.string().optional(), // S3 bucket for storing logs S3_KB_BUCKET_NAME: z.string().optional(), // S3 bucket for knowledge base files + S3_EXECUTION_FILES_BUCKET_NAME: z.string().optional(), // S3 bucket for workflow execution files S3_CHAT_BUCKET_NAME: z.string().optional(), // S3 bucket for chat logos S3_COPILOT_BUCKET_NAME: z.string().optional(), // S3 bucket for copilot files @@ -104,6 +107,7 @@ export const env = createEnv({ AZURE_CONNECTION_STRING: z.string().optional(), // Azure storage connection string AZURE_STORAGE_CONTAINER_NAME: z.string().optional(), // Azure container for general files AZURE_STORAGE_KB_CONTAINER_NAME: z.string().optional(), // Azure container for knowledge base files + AZURE_STORAGE_EXECUTION_FILES_CONTAINER_NAME: z.string().optional(), // Azure container for workflow execution files AZURE_STORAGE_CHAT_CONTAINER_NAME: z.string().optional(), // Azure container for chat logos AZURE_STORAGE_COPILOT_CONTAINER_NAME: z.string().optional(), // Azure container for copilot files diff --git a/apps/sim/lib/logs/execution/logger.ts b/apps/sim/lib/logs/execution/logger.ts index acc68e34cc..8acf498297 100644 --- a/apps/sim/lib/logs/execution/logger.ts +++ b/apps/sim/lib/logs/execution/logger.ts @@ -153,6 +153,9 @@ export class ExecutionLogger implements IExecutionLoggerService { const level = hasErrors ? 'error' : 'info' const message = hasErrors ? 'Workflow execution failed' : 'Workflow execution completed' + // Extract files from trace spans and final output + const executionFiles = this.extractFilesFromExecution(traceSpans, finalOutput) + const [updatedLog] = await db .update(workflowExecutionLogs) .set({ @@ -168,6 +171,7 @@ export class ExecutionLogger implements IExecutionLoggerService { totalInputCost: costSummary.totalInputCost.toString(), totalOutputCost: costSummary.totalOutputCost.toString(), totalTokens: costSummary.totalTokens, + files: executionFiles.length > 0 ? executionFiles : null, metadata: { traceSpans, finalOutput, @@ -414,6 +418,112 @@ export class ExecutionLogger implements IExecutionLoggerService { return 'Unknown' } } + + /** + * Extract file references from execution trace spans and final output + */ + private extractFilesFromExecution(traceSpans?: any[], finalOutput?: any): any[] { + const files: any[] = [] + const seenFileIds = new Set() + + // Helper function to extract files from any object + const extractFilesFromObject = (obj: any, source: string) => { + if (!obj || typeof obj !== 'object') return + + // Check if this object has files property + if (Array.isArray(obj.files)) { + for (const file of obj.files) { + if (file?.name && file.key && file.id) { + if (!seenFileIds.has(file.id)) { + seenFileIds.add(file.id) + files.push({ + id: file.id, + name: file.name, + size: file.size, + type: file.type, + url: file.url, + key: file.key, + uploadedAt: file.uploadedAt, + expiresAt: file.expiresAt, + storageProvider: file.storageProvider, + bucketName: file.bucketName, + }) + } + } + } + } + + // Check if this object has attachments property (for Gmail and other tools) + if (Array.isArray(obj.attachments)) { + for (const file of obj.attachments) { + if (file?.name && file.key && file.id) { + if (!seenFileIds.has(file.id)) { + seenFileIds.add(file.id) + files.push({ + id: file.id, + name: file.name, + size: file.size, + type: file.type, + url: file.url, + key: file.key, + uploadedAt: file.uploadedAt, + expiresAt: file.expiresAt, + storageProvider: file.storageProvider, + bucketName: file.bucketName, + }) + } + } + } + } + + // Check if this object itself is a file reference + if (obj.name && obj.key && typeof obj.size === 'number') { + if (!obj.id) { + logger.warn(`File object missing ID, skipping: ${obj.name}`) + return + } + + if (!seenFileIds.has(obj.id)) { + seenFileIds.add(obj.id) + files.push({ + id: obj.id, + name: obj.name, + size: obj.size, + type: obj.type, + url: obj.url, + key: obj.key, + uploadedAt: obj.uploadedAt, + expiresAt: obj.expiresAt, + storageProvider: obj.storageProvider, + bucketName: obj.bucketName, + }) + } + } + + // Recursively check nested objects and arrays + if (Array.isArray(obj)) { + obj.forEach((item, index) => extractFilesFromObject(item, `${source}[${index}]`)) + } else if (typeof obj === 'object') { + Object.entries(obj).forEach(([key, value]) => { + extractFilesFromObject(value, `${source}.${key}`) + }) + } + } + + // Extract files from trace spans + if (traceSpans && Array.isArray(traceSpans)) { + traceSpans.forEach((span, index) => { + extractFilesFromObject(span, `trace_span_${index}`) + }) + } + + // Extract files from final output + if (finalOutput) { + extractFilesFromObject(finalOutput, 'final_output') + } + + return files + } } export const executionLogger = new ExecutionLogger() diff --git a/apps/sim/lib/logs/types.ts b/apps/sim/lib/logs/types.ts index 389912548e..7d98f4f8b8 100644 --- a/apps/sim/lib/logs/types.ts +++ b/apps/sim/lib/logs/types.ts @@ -95,6 +95,18 @@ export interface WorkflowExecutionLog { totalInputCost: number totalOutputCost: number totalTokens: number + files?: Array<{ + id: string + name: string + size: number + type: string + url: string + key: string + uploadedAt: string + expiresAt: string + storageProvider?: 's3' | 'blob' | 'local' + bucketName?: string + }> metadata: { environment: ExecutionEnvironment trigger: ExecutionTrigger diff --git a/apps/sim/lib/uploads/blob/blob-client.ts b/apps/sim/lib/uploads/blob/blob-client.ts index 955d1a96e8..d81134da29 100644 --- a/apps/sim/lib/uploads/blob/blob-client.ts +++ b/apps/sim/lib/uploads/blob/blob-client.ts @@ -262,9 +262,47 @@ export async function getPresignedUrlWithConfig( * @param key Blob name * @returns File buffer */ -export async function downloadFromBlob(key: string) { - const blobServiceClient = getBlobServiceClient() - const containerClient = blobServiceClient.getContainerClient(BLOB_CONFIG.containerName) +export async function downloadFromBlob(key: string): Promise + +/** + * Download a file from Azure Blob Storage with custom configuration + * @param key Blob name + * @param customConfig Custom Blob configuration + * @returns File buffer + */ +export async function downloadFromBlob(key: string, customConfig: CustomBlobConfig): Promise + +export async function downloadFromBlob( + key: string, + customConfig?: CustomBlobConfig +): Promise { + let blobServiceClient: BlobServiceClient + let containerName: string + + if (customConfig) { + // Use custom configuration + if (customConfig.connectionString) { + blobServiceClient = BlobServiceClient.fromConnectionString(customConfig.connectionString) + } else if (customConfig.accountName && customConfig.accountKey) { + const credential = new StorageSharedKeyCredential( + customConfig.accountName, + customConfig.accountKey + ) + blobServiceClient = new BlobServiceClient( + `https://${customConfig.accountName}.blob.core.windows.net`, + credential + ) + } else { + throw new Error('Invalid custom blob configuration') + } + containerName = customConfig.containerName + } else { + // Use default configuration + blobServiceClient = getBlobServiceClient() + containerName = BLOB_CONFIG.containerName + } + + const containerClient = blobServiceClient.getContainerClient(containerName) const blockBlobClient = containerClient.getBlockBlobClient(key) const downloadBlockBlobResponse = await blockBlobClient.download() @@ -280,9 +318,43 @@ export async function downloadFromBlob(key: string) { * Delete a file from Azure Blob Storage * @param key Blob name */ -export async function deleteFromBlob(key: string) { - const blobServiceClient = getBlobServiceClient() - const containerClient = blobServiceClient.getContainerClient(BLOB_CONFIG.containerName) +export async function deleteFromBlob(key: string): Promise + +/** + * Delete a file from Azure Blob Storage with custom configuration + * @param key Blob name + * @param customConfig Custom Blob configuration + */ +export async function deleteFromBlob(key: string, customConfig: CustomBlobConfig): Promise + +export async function deleteFromBlob(key: string, customConfig?: CustomBlobConfig): Promise { + let blobServiceClient: BlobServiceClient + let containerName: string + + if (customConfig) { + // Use custom configuration + if (customConfig.connectionString) { + blobServiceClient = BlobServiceClient.fromConnectionString(customConfig.connectionString) + } else if (customConfig.accountName && customConfig.accountKey) { + const credential = new StorageSharedKeyCredential( + customConfig.accountName, + customConfig.accountKey + ) + blobServiceClient = new BlobServiceClient( + `https://${customConfig.accountName}.blob.core.windows.net`, + credential + ) + } else { + throw new Error('Invalid custom blob configuration') + } + containerName = customConfig.containerName + } else { + // Use default configuration + blobServiceClient = getBlobServiceClient() + containerName = BLOB_CONFIG.containerName + } + + const containerClient = blobServiceClient.getContainerClient(containerName) const blockBlobClient = containerClient.getBlockBlobClient(key) await blockBlobClient.delete() diff --git a/apps/sim/lib/uploads/index.ts b/apps/sim/lib/uploads/index.ts index 9cf3f6930f..2a345238ec 100644 --- a/apps/sim/lib/uploads/index.ts +++ b/apps/sim/lib/uploads/index.ts @@ -1,10 +1,10 @@ -export * as BlobClient from '@/lib/uploads/blob/blob-client' -export * as S3Client from '@/lib/uploads/s3/s3-client' +// BlobClient and S3Client are server-only - import from specific files when needed +// export * as BlobClient from '@/lib/uploads/blob/blob-client' +// export * as S3Client from '@/lib/uploads/s3/s3-client' export { BLOB_CHAT_CONFIG, BLOB_CONFIG, BLOB_KB_CONFIG, - ensureUploadsDirectory, S3_CHAT_CONFIG, S3_CONFIG, S3_KB_CONFIG, diff --git a/apps/sim/lib/uploads/s3/s3-client.ts b/apps/sim/lib/uploads/s3/s3-client.ts index 1d00d2206a..1eedf52ddd 100644 --- a/apps/sim/lib/uploads/s3/s3-client.ts +++ b/apps/sim/lib/uploads/s3/s3-client.ts @@ -100,6 +100,7 @@ export async function uploadToS3( * @param contentType MIME type of the file * @param customConfig Custom S3 configuration (bucket and region) * @param size File size in bytes (optional, will use buffer length if not provided) + * @param skipTimestampPrefix Skip adding timestamp prefix to filename (default: false) * @returns Object with file information */ export async function uploadToS3( @@ -107,7 +108,8 @@ export async function uploadToS3( fileName: string, contentType: string, customConfig: CustomS3Config, - size?: number + size?: number, + skipTimestampPrefix?: boolean ): Promise export async function uploadToS3( @@ -115,32 +117,29 @@ export async function uploadToS3( fileName: string, contentType: string, configOrSize?: CustomS3Config | number, - size?: number + size?: number, + skipTimestampPrefix?: boolean ): Promise { // Handle overloaded parameters let config: CustomS3Config let fileSize: number + let shouldSkipTimestamp: boolean if (typeof configOrSize === 'object') { // Custom config provided config = configOrSize fileSize = size ?? file.length + shouldSkipTimestamp = skipTimestampPrefix ?? false } else { // Use default config config = { bucket: S3_CONFIG.bucket, region: S3_CONFIG.region } fileSize = configOrSize ?? file.length + shouldSkipTimestamp = size === undefined ? false : (skipTimestampPrefix ?? false) } - // Create a unique filename with timestamp to prevent collisions - // Use a simple timestamp without directory structure + // Create filename - optionally skip timestamp prefix const safeFileName = fileName.replace(/\s+/g, '-') // Replace spaces with hyphens - const uniqueKey = `${Date.now()}-${safeFileName}` - - // Sanitize filename for S3 metadata (only allow ASCII printable characters) - const sanitizedOriginalName = fileName - .replace(/[^\x20-\x7E]/g, '') // Remove non-ASCII characters - .replace(/["\\]/g, '') // Remove quotes and backslashes - .trim() + const uniqueKey = shouldSkipTimestamp ? safeFileName : `${Date.now()}-${safeFileName}` const s3Client = getS3Client() @@ -211,9 +210,21 @@ export async function getPresignedUrlWithConfig( * @param key S3 object key * @returns File buffer */ -export async function downloadFromS3(key: string) { +export async function downloadFromS3(key: string): Promise + +/** + * Download a file from S3 with custom bucket configuration + * @param key S3 object key + * @param customConfig Custom S3 configuration + * @returns File buffer + */ +export async function downloadFromS3(key: string, customConfig: CustomS3Config): Promise + +export async function downloadFromS3(key: string, customConfig?: CustomS3Config): Promise { + const config = customConfig || { bucket: S3_CONFIG.bucket, region: S3_CONFIG.region } + const command = new GetObjectCommand({ - Bucket: S3_CONFIG.bucket, + Bucket: config.bucket, Key: key, }) @@ -257,10 +268,21 @@ export async function downloadFromS3WithConfig(key: string, customConfig: Custom * Delete a file from S3 * @param key S3 object key */ -export async function deleteFromS3(key: string) { +export async function deleteFromS3(key: string): Promise + +/** + * Delete a file from S3 with custom bucket configuration + * @param key S3 object key + * @param customConfig Custom S3 configuration + */ +export async function deleteFromS3(key: string, customConfig: CustomS3Config): Promise + +export async function deleteFromS3(key: string, customConfig?: CustomS3Config): Promise { + const config = customConfig || { bucket: S3_CONFIG.bucket, region: S3_CONFIG.region } + await getS3Client().send( new DeleteObjectCommand({ - Bucket: S3_CONFIG.bucket, + Bucket: config.bucket, Key: key, }) ) diff --git a/apps/sim/lib/uploads/setup.server.ts b/apps/sim/lib/uploads/setup.server.ts index f6cd212d8b..fb65de1ffc 100644 --- a/apps/sim/lib/uploads/setup.server.ts +++ b/apps/sim/lib/uploads/setup.server.ts @@ -1,14 +1,43 @@ +import { existsSync } from 'fs' +import { mkdir } from 'fs/promises' +import path, { join } from 'path' import { env } from '@/lib/env' import { createLogger } from '@/lib/logs/console/logger' -import { - ensureUploadsDirectory, - getStorageProvider, - USE_BLOB_STORAGE, - USE_S3_STORAGE, -} from '@/lib/uploads/setup' +import { getStorageProvider, USE_BLOB_STORAGE, USE_S3_STORAGE } from '@/lib/uploads/setup' const logger = createLogger('UploadsSetup') +// Server-only upload directory path +const PROJECT_ROOT = path.resolve(process.cwd()) +export const UPLOAD_DIR_SERVER = join(PROJECT_ROOT, 'uploads') + +/** + * Server-only function to ensure uploads directory exists + */ +export async function ensureUploadsDirectory() { + if (USE_S3_STORAGE) { + logger.info('Using S3 storage, skipping local uploads directory creation') + return true + } + + if (USE_BLOB_STORAGE) { + logger.info('Using Azure Blob storage, skipping local uploads directory creation') + return true + } + + try { + if (!existsSync(UPLOAD_DIR_SERVER)) { + await mkdir(UPLOAD_DIR_SERVER, { recursive: true }) + } else { + logger.info(`Uploads directory already exists at ${UPLOAD_DIR_SERVER}`) + } + return true + } catch (error) { + logger.error('Failed to create uploads directory:', error) + return false + } +} + // Immediately invoke on server startup if (typeof process !== 'undefined') { const storageProvider = getStorageProvider() diff --git a/apps/sim/lib/uploads/setup.ts b/apps/sim/lib/uploads/setup.ts index 1b9efb6f19..c93143e7cc 100644 --- a/apps/sim/lib/uploads/setup.ts +++ b/apps/sim/lib/uploads/setup.ts @@ -1,14 +1,7 @@ -import { existsSync } from 'fs' -import { mkdir } from 'fs/promises' -import path, { join } from 'path' import { env } from '@/lib/env' -import { createLogger } from '@/lib/logs/console/logger' -const logger = createLogger('UploadsSetup') - -const PROJECT_ROOT = path.resolve(process.cwd()) - -export const UPLOAD_DIR = join(PROJECT_ROOT, 'uploads') +// Client-safe configuration - no Node.js modules +export const UPLOAD_DIR = '/uploads' // Check if S3 is configured (has required credentials) const hasS3Config = !!(env.S3_BUCKET_NAME && env.AWS_REGION) @@ -41,6 +34,11 @@ export const S3_KB_CONFIG = { region: env.AWS_REGION || '', } +export const S3_EXECUTION_FILES_CONFIG = { + bucket: env.S3_EXECUTION_FILES_BUCKET_NAME || 'sim-execution-files', + region: env.AWS_REGION || '', +} + export const BLOB_KB_CONFIG = { accountName: env.AZURE_ACCOUNT_NAME || '', accountKey: env.AZURE_ACCOUNT_KEY || '', @@ -48,6 +46,13 @@ export const BLOB_KB_CONFIG = { containerName: env.AZURE_STORAGE_KB_CONTAINER_NAME || '', } +export const BLOB_EXECUTION_FILES_CONFIG = { + accountName: env.AZURE_ACCOUNT_NAME || '', + accountKey: env.AZURE_ACCOUNT_KEY || '', + connectionString: env.AZURE_CONNECTION_STRING || '', + containerName: env.AZURE_STORAGE_EXECUTION_FILES_CONTAINER_NAME || 'sim-execution-files', +} + export const S3_CHAT_CONFIG = { bucket: env.S3_CHAT_BUCKET_NAME || '', region: env.AWS_REGION || '', @@ -72,30 +77,6 @@ export const BLOB_COPILOT_CONFIG = { containerName: env.AZURE_STORAGE_COPILOT_CONTAINER_NAME || '', } -export async function ensureUploadsDirectory() { - if (USE_S3_STORAGE) { - logger.info('Using S3 storage, skipping local uploads directory creation') - return true - } - - if (USE_BLOB_STORAGE) { - logger.info('Using Azure Blob storage, skipping local uploads directory creation') - return true - } - - try { - if (!existsSync(UPLOAD_DIR)) { - await mkdir(UPLOAD_DIR, { recursive: true }) - } else { - logger.info(`Uploads directory already exists at ${UPLOAD_DIR}`) - } - return true - } catch (error) { - logger.error('Failed to create uploads directory:', error) - return false - } -} - /** * Get the current storage provider as a human-readable string */ diff --git a/apps/sim/lib/uploads/storage-client.ts b/apps/sim/lib/uploads/storage-client.ts index 0965e07dce..469323b54f 100644 --- a/apps/sim/lib/uploads/storage-client.ts +++ b/apps/sim/lib/uploads/storage-client.ts @@ -1,28 +1,29 @@ import { createLogger } from '@/lib/logs/console/logger' -import { - type FileInfo as BlobFileInfo, - type CustomBlobConfig, - deleteFromBlob, - downloadFromBlob, - getPresignedUrl as getBlobPresignedUrl, - getPresignedUrlWithConfig as getBlobPresignedUrlWithConfig, - uploadToBlob, -} from '@/lib/uploads/blob/blob-client' -import { - type CustomS3Config, - deleteFromS3, - downloadFromS3, - getPresignedUrl as getS3PresignedUrl, - getPresignedUrlWithConfig as getS3PresignedUrlWithConfig, - type FileInfo as S3FileInfo, - uploadToS3, -} from '@/lib/uploads/s3/s3-client' +import type { CustomBlobConfig } from '@/lib/uploads/blob/blob-client' +import type { CustomS3Config } from '@/lib/uploads/s3/s3-client' import { USE_BLOB_STORAGE, USE_S3_STORAGE } from '@/lib/uploads/setup' const logger = createLogger('StorageClient') -export type FileInfo = S3FileInfo | BlobFileInfo -export type CustomStorageConfig = CustomS3Config | CustomBlobConfig +// Client-safe type definitions +export type FileInfo = { + path: string + key: string + name: string + size: number + type: string +} + +export type CustomStorageConfig = { + // S3 config + bucket?: string + region?: string + // Blob config + containerName?: string + accountName?: string + accountKey?: string + connectionString?: string +} /** * Upload a file to the configured storage provider @@ -65,16 +66,28 @@ export async function uploadFile( ): Promise { if (USE_BLOB_STORAGE) { logger.info(`Uploading file to Azure Blob Storage: ${fileName}`) + const { uploadToBlob } = await import('@/lib/uploads/blob/blob-client') if (typeof configOrSize === 'object') { - return uploadToBlob(file, fileName, contentType, configOrSize as CustomBlobConfig, size) + const blobConfig: CustomBlobConfig = { + containerName: configOrSize.containerName!, + accountName: configOrSize.accountName!, + accountKey: configOrSize.accountKey, + connectionString: configOrSize.connectionString, + } + return uploadToBlob(file, fileName, contentType, blobConfig, size) } return uploadToBlob(file, fileName, contentType, configOrSize) } if (USE_S3_STORAGE) { logger.info(`Uploading file to S3: ${fileName}`) + const { uploadToS3 } = await import('@/lib/uploads/s3/s3-client') if (typeof configOrSize === 'object') { - return uploadToS3(file, fileName, contentType, configOrSize as CustomS3Config, size) + const s3Config: CustomS3Config = { + bucket: configOrSize.bucket!, + region: configOrSize.region!, + } + return uploadToS3(file, fileName, contentType, s3Config, size) } return uploadToS3(file, fileName, contentType, configOrSize) } @@ -92,11 +105,13 @@ export async function uploadFile( export async function downloadFile(key: string): Promise { if (USE_BLOB_STORAGE) { logger.info(`Downloading file from Azure Blob Storage: ${key}`) + const { downloadFromBlob } = await import('@/lib/uploads/blob/blob-client') return downloadFromBlob(key) } if (USE_S3_STORAGE) { logger.info(`Downloading file from S3: ${key}`) + const { downloadFromS3 } = await import('@/lib/uploads/s3/s3-client') return downloadFromS3(key) } @@ -112,11 +127,13 @@ export async function downloadFile(key: string): Promise { export async function deleteFile(key: string): Promise { if (USE_BLOB_STORAGE) { logger.info(`Deleting file from Azure Blob Storage: ${key}`) + const { deleteFromBlob } = await import('@/lib/uploads/blob/blob-client') return deleteFromBlob(key) } if (USE_S3_STORAGE) { logger.info(`Deleting file from S3: ${key}`) + const { deleteFromS3 } = await import('@/lib/uploads/s3/s3-client') return deleteFromS3(key) } @@ -134,11 +151,13 @@ export async function deleteFile(key: string): Promise { export async function getPresignedUrl(key: string, expiresIn = 3600): Promise { if (USE_BLOB_STORAGE) { logger.info(`Generating presigned URL for Azure Blob Storage: ${key}`) + const { getPresignedUrl: getBlobPresignedUrl } = await import('@/lib/uploads/blob/blob-client') return getBlobPresignedUrl(key, expiresIn) } if (USE_S3_STORAGE) { logger.info(`Generating presigned URL for S3: ${key}`) + const { getPresignedUrl: getS3PresignedUrl } = await import('@/lib/uploads/s3/s3-client') return getS3PresignedUrl(key, expiresIn) } @@ -161,12 +180,30 @@ export async function getPresignedUrlWithConfig( ): Promise { if (USE_BLOB_STORAGE) { logger.info(`Generating presigned URL for Azure Blob Storage with custom config: ${key}`) - return getBlobPresignedUrlWithConfig(key, customConfig as CustomBlobConfig, expiresIn) + const { getPresignedUrlWithConfig: getBlobPresignedUrlWithConfig } = await import( + '@/lib/uploads/blob/blob-client' + ) + // Convert CustomStorageConfig to CustomBlobConfig + const blobConfig: CustomBlobConfig = { + containerName: customConfig.containerName!, + accountName: customConfig.accountName!, + accountKey: customConfig.accountKey, + connectionString: customConfig.connectionString, + } + return getBlobPresignedUrlWithConfig(key, blobConfig, expiresIn) } if (USE_S3_STORAGE) { logger.info(`Generating presigned URL for S3 with custom config: ${key}`) - return getS3PresignedUrlWithConfig(key, customConfig as CustomS3Config, expiresIn) + const { getPresignedUrlWithConfig: getS3PresignedUrlWithConfig } = await import( + '@/lib/uploads/s3/s3-client' + ) + // Convert CustomStorageConfig to CustomS3Config + const s3Config: CustomS3Config = { + bucket: customConfig.bucket!, + region: customConfig.region!, + } + return getS3PresignedUrlWithConfig(key, s3Config, expiresIn) } throw new Error( diff --git a/apps/sim/lib/workflows/db-helpers.ts b/apps/sim/lib/workflows/db-helpers.ts index 2073dc65c8..8e2bee03ac 100644 --- a/apps/sim/lib/workflows/db-helpers.ts +++ b/apps/sim/lib/workflows/db-helpers.ts @@ -103,6 +103,7 @@ export async function loadWorkflowFromNormalizedTables( horizontalHandles: block.horizontalHandles, isWide: block.isWide, advancedMode: block.advancedMode, + triggerMode: block.triggerMode, height: Number(block.height), subBlocks: block.subBlocks || {}, outputs: block.outputs || {}, @@ -188,6 +189,7 @@ export async function saveWorkflowToNormalizedTables( horizontalHandles: block.horizontalHandles ?? true, isWide: block.isWide ?? false, advancedMode: block.advancedMode ?? false, + triggerMode: block.triggerMode ?? false, height: String(block.height || 0), subBlocks: block.subBlocks || {}, outputs: block.outputs || {}, diff --git a/apps/sim/lib/workflows/execution-file-storage.ts b/apps/sim/lib/workflows/execution-file-storage.ts new file mode 100644 index 0000000000..7636209177 --- /dev/null +++ b/apps/sim/lib/workflows/execution-file-storage.ts @@ -0,0 +1,257 @@ +/** + * Specialized storage client for workflow execution files + * Uses dedicated S3 bucket: sim-execution-files + * Directory structure: workspace_id/workflow_id/execution_id/filename + */ + +import { createLogger } from '@/lib/logs/console/logger' +import { + deleteFromBlob, + downloadFromBlob, + getPresignedUrlWithConfig as getBlobPresignedUrlWithConfig, + uploadToBlob, +} from '@/lib/uploads/blob/blob-client' +import { + deleteFromS3, + downloadFromS3, + getPresignedUrlWithConfig, + uploadToS3, +} from '@/lib/uploads/s3/s3-client' +import { + BLOB_EXECUTION_FILES_CONFIG, + S3_EXECUTION_FILES_CONFIG, + USE_BLOB_STORAGE, + USE_S3_STORAGE, +} from '@/lib/uploads/setup' +import type { UserFile } from '@/executor/types' +import type { ExecutionContext } from './execution-files' +import { generateExecutionFileKey, generateFileId, getFileExpirationDate } from './execution-files' + +const logger = createLogger('ExecutionFileStorage') + +/** + * Upload a file to execution-scoped storage + */ +export async function uploadExecutionFile( + context: ExecutionContext, + fileBuffer: Buffer, + fileName: string, + contentType: string +): Promise { + logger.info(`Uploading execution file: ${fileName} for execution ${context.executionId}`) + logger.debug(`File upload context:`, { + workspaceId: context.workspaceId, + workflowId: context.workflowId, + executionId: context.executionId, + fileName, + bufferSize: fileBuffer.length, + }) + + // Generate execution-scoped storage key + const storageKey = generateExecutionFileKey(context, fileName) + const fileId = generateFileId() + + logger.info(`Generated storage key: "${storageKey}" for file: ${fileName}`) + + try { + let fileInfo: any + let directUrl: string | undefined + + if (USE_S3_STORAGE) { + // Upload to S3 execution files bucket with exact key (no timestamp prefix) + logger.debug( + `Uploading to S3 with key: ${storageKey}, bucket: ${S3_EXECUTION_FILES_CONFIG.bucket}` + ) + fileInfo = await uploadToS3( + fileBuffer, + storageKey, // Use storageKey as fileName + contentType, + { + bucket: S3_EXECUTION_FILES_CONFIG.bucket, + region: S3_EXECUTION_FILES_CONFIG.region, + }, + undefined, // size (will use buffer length) + true // skipTimestampPrefix = true + ) + + logger.info(`S3 upload returned key: "${fileInfo.key}" for file: ${fileName}`) + logger.info(`Original storage key was: "${storageKey}"`) + logger.info(`Keys match: ${fileInfo.key === storageKey}`) + + // Generate presigned URL for execution (5 minutes) + try { + logger.info(`Generating presigned URL with key: "${fileInfo.key}"`) + directUrl = await getPresignedUrlWithConfig( + fileInfo.key, // Use the actual uploaded key + { + bucket: S3_EXECUTION_FILES_CONFIG.bucket, + region: S3_EXECUTION_FILES_CONFIG.region, + }, + 5 * 60 // 5 minutes + ) + logger.info(`Generated presigned URL: ${directUrl}`) + } catch (error) { + logger.warn(`Failed to generate S3 presigned URL for ${fileName}:`, error) + } + } else if (USE_BLOB_STORAGE) { + // Upload to Azure Blob execution files container + fileInfo = await uploadToBlob(fileBuffer, storageKey, contentType, { + accountName: BLOB_EXECUTION_FILES_CONFIG.accountName, + accountKey: BLOB_EXECUTION_FILES_CONFIG.accountKey, + connectionString: BLOB_EXECUTION_FILES_CONFIG.connectionString, + containerName: BLOB_EXECUTION_FILES_CONFIG.containerName, + }) + + // Generate presigned URL for execution (5 minutes) + try { + directUrl = await getBlobPresignedUrlWithConfig( + fileInfo.key, // Use the actual uploaded key + { + accountName: BLOB_EXECUTION_FILES_CONFIG.accountName, + accountKey: BLOB_EXECUTION_FILES_CONFIG.accountKey, + connectionString: BLOB_EXECUTION_FILES_CONFIG.connectionString, + containerName: BLOB_EXECUTION_FILES_CONFIG.containerName, + }, + 5 * 60 // 5 minutes + ) + } catch (error) { + logger.warn(`Failed to generate Blob presigned URL for ${fileName}:`, error) + } + } else { + throw new Error('No cloud storage configured for execution files') + } + + const userFile: UserFile = { + id: fileId, + name: fileName, + size: fileBuffer.length, + type: contentType, + url: directUrl || `/api/files/serve/${fileInfo.key}`, // Use 5-minute presigned URL, fallback to serve path + key: fileInfo.key, // Use the actual uploaded key from S3/Blob + uploadedAt: new Date().toISOString(), + expiresAt: getFileExpirationDate(), + } + + logger.info(`Successfully uploaded execution file: ${fileName} (${fileBuffer.length} bytes)`) + return userFile + } catch (error) { + logger.error(`Failed to upload execution file ${fileName}:`, error) + throw new Error( + `Failed to upload file: ${error instanceof Error ? error.message : 'Unknown error'}` + ) + } +} + +/** + * Download a file from execution-scoped storage + */ +export async function downloadExecutionFile(userFile: UserFile): Promise { + logger.info(`Downloading execution file: ${userFile.name}`) + + try { + let fileBuffer: Buffer + + if (USE_S3_STORAGE) { + fileBuffer = await downloadFromS3(userFile.key, { + bucket: S3_EXECUTION_FILES_CONFIG.bucket, + region: S3_EXECUTION_FILES_CONFIG.region, + }) + } else if (USE_BLOB_STORAGE) { + fileBuffer = await downloadFromBlob(userFile.key, { + accountName: BLOB_EXECUTION_FILES_CONFIG.accountName, + accountKey: BLOB_EXECUTION_FILES_CONFIG.accountKey, + connectionString: BLOB_EXECUTION_FILES_CONFIG.connectionString, + containerName: BLOB_EXECUTION_FILES_CONFIG.containerName, + }) + } else { + throw new Error('No cloud storage configured for execution files') + } + + logger.info( + `Successfully downloaded execution file: ${userFile.name} (${fileBuffer.length} bytes)` + ) + return fileBuffer + } catch (error) { + logger.error(`Failed to download execution file ${userFile.name}:`, error) + throw new Error( + `Failed to download file: ${error instanceof Error ? error.message : 'Unknown error'}` + ) + } +} + +/** + * Generate a short-lived presigned URL for file download (5 minutes) + */ +export async function generateExecutionFileDownloadUrl(userFile: UserFile): Promise { + logger.info(`Generating download URL for execution file: ${userFile.name}`) + logger.info(`File key: "${userFile.key}"`) + logger.info(`S3 bucket: ${S3_EXECUTION_FILES_CONFIG.bucket}`) + + try { + let downloadUrl: string + + if (USE_S3_STORAGE) { + downloadUrl = await getPresignedUrlWithConfig( + userFile.key, + { + bucket: S3_EXECUTION_FILES_CONFIG.bucket, + region: S3_EXECUTION_FILES_CONFIG.region, + }, + 5 * 60 // 5 minutes + ) + } else if (USE_BLOB_STORAGE) { + downloadUrl = await getBlobPresignedUrlWithConfig( + userFile.key, + { + accountName: BLOB_EXECUTION_FILES_CONFIG.accountName, + accountKey: BLOB_EXECUTION_FILES_CONFIG.accountKey, + connectionString: BLOB_EXECUTION_FILES_CONFIG.connectionString, + containerName: BLOB_EXECUTION_FILES_CONFIG.containerName, + }, + 5 * 60 // 5 minutes + ) + } else { + throw new Error('No cloud storage configured for execution files') + } + + logger.info(`Generated download URL for execution file: ${userFile.name}`) + return downloadUrl + } catch (error) { + logger.error(`Failed to generate download URL for ${userFile.name}:`, error) + throw new Error( + `Failed to generate download URL: ${error instanceof Error ? error.message : 'Unknown error'}` + ) + } +} + +/** + * Delete a file from execution-scoped storage + */ +export async function deleteExecutionFile(userFile: UserFile): Promise { + logger.info(`Deleting execution file: ${userFile.name}`) + + try { + if (USE_S3_STORAGE) { + await deleteFromS3(userFile.key, { + bucket: S3_EXECUTION_FILES_CONFIG.bucket, + region: S3_EXECUTION_FILES_CONFIG.region, + }) + } else if (USE_BLOB_STORAGE) { + await deleteFromBlob(userFile.key, { + accountName: BLOB_EXECUTION_FILES_CONFIG.accountName, + accountKey: BLOB_EXECUTION_FILES_CONFIG.accountKey, + connectionString: BLOB_EXECUTION_FILES_CONFIG.connectionString, + containerName: BLOB_EXECUTION_FILES_CONFIG.containerName, + }) + } else { + throw new Error('No cloud storage configured for execution files') + } + + logger.info(`Successfully deleted execution file: ${userFile.name}`) + } catch (error) { + logger.error(`Failed to delete execution file ${userFile.name}:`, error) + throw new Error( + `Failed to delete file: ${error instanceof Error ? error.message : 'Unknown error'}` + ) + } +} diff --git a/apps/sim/lib/workflows/execution-files-server.ts b/apps/sim/lib/workflows/execution-files-server.ts new file mode 100644 index 0000000000..235710d79b --- /dev/null +++ b/apps/sim/lib/workflows/execution-files-server.ts @@ -0,0 +1,147 @@ +/** + * Server-only execution file metadata management + * This file contains database operations and should only be imported by server-side code + */ + +import { eq } from 'drizzle-orm' +import { createLogger } from '@/lib/logs/console/logger' +import { db } from '@/db' +import { workflowExecutionLogs } from '@/db/schema' +import type { ExecutionFileMetadata } from './execution-files' + +const logger = createLogger('ExecutionFilesServer') + +/** + * Retrieve file metadata from execution logs + */ +export async function getExecutionFiles(executionId: string): Promise { + try { + const log = await db + .select() + .from(workflowExecutionLogs) + .where(eq(workflowExecutionLogs.executionId, executionId)) + .limit(1) + + if (log.length === 0) { + return [] + } + + // Get files from the dedicated files column + return (log[0].files as ExecutionFileMetadata[]) || [] + } catch (error) { + logger.error(`Failed to retrieve file metadata for execution ${executionId}:`, error) + return [] + } +} + +/** + * Store file metadata in execution logs + */ +export async function storeExecutionFiles( + executionId: string, + files: ExecutionFileMetadata[] +): Promise { + try { + logger.info(`Storing ${files.length} file metadata entries for execution ${executionId}`) + + await db + .update(workflowExecutionLogs) + .set({ files }) + .where(eq(workflowExecutionLogs.executionId, executionId)) + + logger.info(`Successfully stored file metadata for execution ${executionId}`) + } catch (error) { + logger.error(`Failed to store file metadata for execution ${executionId}:`, error) + throw error + } +} + +/** + * Add file metadata to existing execution logs + */ +export async function addExecutionFile( + executionId: string, + fileMetadata: ExecutionFileMetadata +): Promise { + try { + // Get existing files + const existingFiles = await getExecutionFiles(executionId) + + // Add new file + const updatedFiles = [...existingFiles, fileMetadata] + + // Store updated files + await storeExecutionFiles(executionId, updatedFiles) + + logger.info(`Added file ${fileMetadata.name} to execution ${executionId}`) + } catch (error) { + logger.error(`Failed to add file to execution ${executionId}:`, error) + throw error + } +} + +/** + * Get all expired files across all executions + */ +export async function getExpiredFiles(): Promise { + try { + const now = new Date().toISOString() + + // Query all execution logs that have files + const logs = await db + .select() + .from(workflowExecutionLogs) + .where(eq(workflowExecutionLogs.level, 'info')) // Only get successful executions + + const expiredFiles: ExecutionFileMetadata[] = [] + + for (const log of logs) { + const files = log.files as ExecutionFileMetadata[] + if (files) { + const expired = files.filter((file) => file.expiresAt < now) + expiredFiles.push(...expired) + } + } + + return expiredFiles + } catch (error) { + logger.error('Failed to get expired files:', error) + return [] + } +} + +/** + * Remove expired file metadata from execution logs + */ +export async function cleanupExpiredFileMetadata(): Promise { + try { + const now = new Date().toISOString() + let cleanedCount = 0 + + // Get all execution logs + const logs = await db.select().from(workflowExecutionLogs) + + for (const log of logs) { + const files = log.files as ExecutionFileMetadata[] + if (files && files.length > 0) { + const nonExpiredFiles = files.filter((file) => file.expiresAt >= now) + + if (nonExpiredFiles.length !== files.length) { + // Some files expired, update the files column + await db + .update(workflowExecutionLogs) + .set({ files: nonExpiredFiles.length > 0 ? nonExpiredFiles : null }) + .where(eq(workflowExecutionLogs.id, log.id)) + + cleanedCount += files.length - nonExpiredFiles.length + } + } + } + + logger.info(`Cleaned up ${cleanedCount} expired file metadata entries`) + return cleanedCount + } catch (error) { + logger.error('Failed to cleanup expired file metadata:', error) + return 0 + } +} diff --git a/apps/sim/lib/workflows/execution-files.ts b/apps/sim/lib/workflows/execution-files.ts new file mode 100644 index 0000000000..7f6fdd60fc --- /dev/null +++ b/apps/sim/lib/workflows/execution-files.ts @@ -0,0 +1,60 @@ +/** + * Execution file management system for binary data transfer between blocks + * This handles file storage, retrieval, and cleanup for workflow executions + */ + +import type { UserFile } from '@/executor/types' + +/** + * Execution context for file operations + */ +export interface ExecutionContext { + workspaceId: string + workflowId: string + executionId: string +} + +/** + * File metadata stored in execution logs - now just uses UserFile directly + */ +export type ExecutionFileMetadata = UserFile + +/** + * Generate execution-scoped storage key + * Format: workspace_id/workflow_id/execution_id/filename + */ +export function generateExecutionFileKey(context: ExecutionContext, fileName: string): string { + const { workspaceId, workflowId, executionId } = context + const safeFileName = fileName.replace(/\s+/g, '-').replace(/[^a-zA-Z0-9.-]/g, '_') + return `${workspaceId}/${workflowId}/${executionId}/${safeFileName}` +} + +/** + * Generate execution prefix for cleanup operations + * Format: workspace_id/workflow_id/execution_id/ + */ +export function generateExecutionPrefix(context: ExecutionContext): string { + const { workspaceId, workflowId, executionId } = context + return `${workspaceId}/${workflowId}/${executionId}/` +} + +/** + * Generate unique file ID for execution files + */ +export function generateFileId(): string { + return `file_${Date.now()}_${Math.random().toString(36).substring(2, 9)}` +} + +/** + * Check if a user file is expired + */ +export function isFileExpired(userFile: UserFile): boolean { + return new Date(userFile.expiresAt) < new Date() +} + +/** + * Get file expiration date for execution files (5 minutes from now) + */ +export function getFileExpirationDate(): string { + return new Date(Date.now() + 5 * 60 * 1000).toISOString() +} diff --git a/apps/sim/middleware.ts b/apps/sim/middleware.ts index 7f6b354093..d69f970955 100644 --- a/apps/sim/middleware.ts +++ b/apps/sim/middleware.ts @@ -93,6 +93,13 @@ export async function middleware(request: NextRequest) { if (!hasActiveSession) { return NextResponse.redirect(new URL('/login', request.url)) } + + // Check if user needs email verification + const requiresVerification = request.cookies.get('requiresEmailVerification') + if (requiresVerification?.value === 'true') { + return NextResponse.redirect(new URL('/verify', request.url)) + } + return NextResponse.next() } diff --git a/apps/sim/package.json b/apps/sim/package.json index aa39154b97..da697019a6 100644 --- a/apps/sim/package.json +++ b/apps/sim/package.json @@ -133,7 +133,7 @@ "@types/js-yaml": "4.0.9", "@types/jsdom": "21.1.7", "@types/lodash": "^4.17.16", - "@types/node": "^22", + "@types/node": "24.2.1", "@types/prismjs": "^1.26.5", "@types/react": "^19", "@types/react-dom": "^19", diff --git a/apps/sim/scripts/migrate-workflow-states.ts b/apps/sim/scripts/migrate-workflow-states.ts index 87e5e0252f..fb81ac19fd 100755 --- a/apps/sim/scripts/migrate-workflow-states.ts +++ b/apps/sim/scripts/migrate-workflow-states.ts @@ -130,6 +130,7 @@ async function migrateWorkflowStates(specificWorkflowIds?: string[] | null) { horizontalHandles: block.horizontalHandles ?? true, isWide: block.isWide ?? false, advancedMode: block.advancedMode ?? false, + triggerMode: block.triggerMode ?? false, height: String(block.height || 0), subBlocks: block.subBlocks || {}, outputs: block.outputs || {}, diff --git a/apps/sim/socket-server/database/operations.ts b/apps/sim/socket-server/database/operations.ts index 4b12cb6b7a..0cf9f8489c 100644 --- a/apps/sim/socket-server/database/operations.ts +++ b/apps/sim/socket-server/database/operations.ts @@ -560,6 +560,28 @@ async function handleBlockOperationTx( break } + case 'update-trigger-mode': { + if (!payload.id || payload.triggerMode === undefined) { + throw new Error('Missing required fields for update trigger mode operation') + } + + const updateResult = await tx + .update(workflowBlocks) + .set({ + triggerMode: payload.triggerMode, + updatedAt: new Date(), + }) + .where(and(eq(workflowBlocks.id, payload.id), eq(workflowBlocks.workflowId, workflowId))) + .returning({ id: workflowBlocks.id }) + + if (updateResult.length === 0) { + throw new Error(`Block ${payload.id} not found in workflow ${workflowId}`) + } + + logger.debug(`Updated block trigger mode: ${payload.id} -> ${payload.triggerMode}`) + break + } + case 'toggle-handles': { if (!payload.id || payload.horizontalHandles === undefined) { throw new Error('Missing required fields for toggle handles operation') diff --git a/apps/sim/socket-server/middleware/permissions.ts b/apps/sim/socket-server/middleware/permissions.ts index 3be2c835ca..a94ae21b1e 100644 --- a/apps/sim/socket-server/middleware/permissions.ts +++ b/apps/sim/socket-server/middleware/permissions.ts @@ -102,6 +102,7 @@ export async function verifyOperationPermission( 'update-parent', 'update-wide', 'update-advanced-mode', + 'update-trigger-mode', 'toggle-handles', 'duplicate', ], @@ -115,6 +116,7 @@ export async function verifyOperationPermission( 'update-parent', 'update-wide', 'update-advanced-mode', + 'update-trigger-mode', 'toggle-handles', 'duplicate', ], diff --git a/apps/sim/socket-server/validation/schemas.ts b/apps/sim/socket-server/validation/schemas.ts index c36cc0ca2b..d7bee359da 100644 --- a/apps/sim/socket-server/validation/schemas.ts +++ b/apps/sim/socket-server/validation/schemas.ts @@ -25,6 +25,7 @@ export const BlockOperationSchema = z.object({ 'update-parent', 'update-wide', 'update-advanced-mode', + 'update-trigger-mode', 'toggle-handles', 'duplicate', ]), @@ -44,6 +45,7 @@ export const BlockOperationSchema = z.object({ horizontalHandles: z.boolean().optional(), isWide: z.boolean().optional(), advancedMode: z.boolean().optional(), + triggerMode: z.boolean().optional(), height: z.number().optional(), autoConnectEdge: AutoConnectEdgeSchema.optional(), // Add support for auto-connect edges }), diff --git a/apps/sim/stores/logs/filters/types.ts b/apps/sim/stores/logs/filters/types.ts index 4a1bb636de..80d2a255c0 100644 --- a/apps/sim/stores/logs/filters/types.ts +++ b/apps/sim/stores/logs/filters/types.ts @@ -78,6 +78,18 @@ export interface WorkflowLog { trigger: string | null createdAt: string workflow?: WorkflowData | null + files?: Array<{ + id: string + name: string + size: number + type: string + url: string + key: string + uploadedAt: string + expiresAt: string + storageProvider?: 's3' | 'blob' | 'local' + bucketName?: string + }> metadata?: ToolCallMetadata & { traceSpans?: TraceSpan[] totalDuration?: number diff --git a/apps/sim/stores/organization/utils.ts b/apps/sim/stores/organization/utils.ts index 9ae629b7cd..46d3abb6d7 100644 --- a/apps/sim/stores/organization/utils.ts +++ b/apps/sim/stores/organization/utils.ts @@ -1,3 +1,4 @@ +import { quickValidateEmail } from '@/lib/email/validation' import type { Organization } from '@/stores/organization/types' /** @@ -32,5 +33,5 @@ export function validateSlug(slug: string): boolean { * Validate email format */ export function validateEmail(email: string): boolean { - return email.includes('@') && email.trim().length > 0 + return quickValidateEmail(email.trim().toLowerCase()).isValid } diff --git a/apps/sim/stores/workflows/registry/store.ts b/apps/sim/stores/workflows/registry/store.ts index 762ef9c8d0..19ef7b9659 100644 --- a/apps/sim/stores/workflows/registry/store.ts +++ b/apps/sim/stores/workflows/registry/store.ts @@ -731,6 +731,7 @@ export const useWorkflowRegistry = create()( horizontalHandles: true, isWide: false, advancedMode: false, + triggerMode: false, height: 0, } @@ -1107,6 +1108,7 @@ export const useWorkflowRegistry = create()( horizontalHandles: true, isWide: false, advancedMode: false, + triggerMode: false, height: 0, } diff --git a/apps/sim/stores/workflows/workflow/store.ts b/apps/sim/stores/workflows/workflow/store.ts index e9b147f590..a12426d8c2 100644 --- a/apps/sim/stores/workflows/workflow/store.ts +++ b/apps/sim/stores/workflows/workflow/store.ts @@ -101,6 +101,7 @@ export const useWorkflowStore = create()( horizontalHandles?: boolean isWide?: boolean advancedMode?: boolean + triggerMode?: boolean height?: number } ) => { @@ -127,6 +128,7 @@ export const useWorkflowStore = create()( horizontalHandles: blockProperties?.horizontalHandles ?? true, isWide: blockProperties?.isWide ?? false, advancedMode: blockProperties?.advancedMode ?? false, + triggerMode: blockProperties?.triggerMode ?? false, height: blockProperties?.height ?? 0, data: nodeData, }, @@ -177,6 +179,7 @@ export const useWorkflowStore = create()( horizontalHandles: blockProperties?.horizontalHandles ?? true, isWide: blockProperties?.isWide ?? false, advancedMode: blockProperties?.advancedMode ?? false, + triggerMode: blockProperties?.triggerMode ?? false, height: blockProperties?.height ?? 0, data: nodeData, }, @@ -746,6 +749,22 @@ export const useWorkflowStore = create()( // Note: Socket.IO handles real-time sync automatically }, + setBlockTriggerMode: (id: string, triggerMode: boolean) => { + set((state) => ({ + blocks: { + ...state.blocks, + [id]: { + ...state.blocks[id], + triggerMode, + }, + }, + edges: [...state.edges], + loops: { ...state.loops }, + })) + get().updateLastSaved() + // Note: Socket.IO handles real-time sync automatically + }, + updateBlockHeight: (id: string, height: number) => { set((state) => ({ blocks: { @@ -991,6 +1010,85 @@ export const useWorkflowStore = create()( // Note: Socket.IO handles real-time sync automatically }, + toggleBlockTriggerMode: (id: string) => { + const block = get().blocks[id] + if (!block) return + + const newTriggerMode = !block.triggerMode + + // When switching TO trigger mode, remove all incoming connections + let filteredEdges = [...get().edges] + if (newTriggerMode) { + // Remove edges where this block is the target + filteredEdges = filteredEdges.filter((edge) => edge.target !== id) + logger.info( + `Removed ${get().edges.length - filteredEdges.length} incoming connections for trigger mode`, + { + blockId: id, + blockType: block.type, + } + ) + } + + const newState = { + blocks: { + ...get().blocks, + [id]: { + ...block, + triggerMode: newTriggerMode, + }, + }, + edges: filteredEdges, + loops: { ...get().loops }, + parallels: { ...get().parallels }, + } + + set(newState) + pushHistory(set, get, newState, `Toggle trigger mode for ${block.type} block`) + get().updateLastSaved() + + // Handle webhook enable/disable when toggling trigger mode + const handleWebhookToggle = async () => { + try { + const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId + if (!activeWorkflowId) return + + // Check if there's a webhook for this block + const response = await fetch( + `/api/webhooks?workflowId=${activeWorkflowId}&blockId=${id}` + ) + if (response.ok) { + const data = await response.json() + if (data.webhooks && data.webhooks.length > 0) { + const webhook = data.webhooks[0].webhook + + // Update webhook's isActive status based on trigger mode + const updateResponse = await fetch(`/api/webhooks/${webhook.id}`, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + isActive: newTriggerMode, + }), + }) + + if (!updateResponse.ok) { + console.error('Failed to update webhook status') + } + } + } + } catch (error) { + console.error('Error toggling webhook status:', error) + } + } + + // Handle webhook toggle asynchronously + handleWebhookToggle() + + // Note: Socket.IO handles real-time sync automatically + }, + // Parallel block methods implementation updateParallelCount: (parallelId: string, count: number) => { const block = get().blocks[parallelId] diff --git a/apps/sim/stores/workflows/workflow/types.ts b/apps/sim/stores/workflows/workflow/types.ts index 3ccc21637d..42d12ba0db 100644 --- a/apps/sim/stores/workflows/workflow/types.ts +++ b/apps/sim/stores/workflows/workflow/types.ts @@ -74,6 +74,7 @@ export interface BlockState { isWide?: boolean height?: number advancedMode?: boolean + triggerMode?: boolean data?: BlockData } @@ -167,6 +168,7 @@ export interface WorkflowActions { horizontalHandles?: boolean isWide?: boolean advancedMode?: boolean + triggerMode?: boolean height?: number } ) => void @@ -185,6 +187,7 @@ export interface WorkflowActions { toggleBlockWide: (id: string) => void setBlockWide: (id: string, isWide: boolean) => void setBlockAdvancedMode: (id: string, advancedMode: boolean) => void + setBlockTriggerMode: (id: string, triggerMode: boolean) => void updateBlockHeight: (id: string, height: number) => void triggerUpdate: () => void updateLoopCount: (loopId: string, count: number) => void @@ -199,6 +202,7 @@ export interface WorkflowActions { setWebhookStatus: (hasActiveWebhook: boolean) => void revertToDeployedState: (deployedState: WorkflowState) => void toggleBlockAdvancedMode: (id: string) => void + toggleBlockTriggerMode: (id: string) => void // Add the sync control methods to the WorkflowActions interface sync: SyncControl diff --git a/apps/sim/tools/airtable/create_records.ts b/apps/sim/tools/airtable/create_records.ts index 7f7e8bee36..c88e575c85 100644 --- a/apps/sim/tools/airtable/create_records.ts +++ b/apps/sim/tools/airtable/create_records.ts @@ -69,4 +69,23 @@ export const airtableCreateRecordsTool: ToolConfig { return `Failed to create Airtable records: ${error.message || 'Unknown error'}` }, + + outputs: { + records: { + type: 'json', + description: 'Array of created Airtable records', + items: { + type: 'object', + properties: { + id: { type: 'string' }, + createdTime: { type: 'string' }, + fields: { type: 'object' }, + }, + }, + }, + metadata: { + type: 'json', + description: 'Operation metadata', + }, + }, } diff --git a/apps/sim/tools/airtable/get_record.ts b/apps/sim/tools/airtable/get_record.ts index a789575e8a..206a578ea5 100644 --- a/apps/sim/tools/airtable/get_record.ts +++ b/apps/sim/tools/airtable/get_record.ts @@ -72,4 +72,15 @@ export const airtableGetRecordTool: ToolConfig { return `Failed to list Airtable records: ${error.message || 'Unknown error'}` }, + + outputs: { + records: { + type: 'json', + description: 'Array of retrieved Airtable records', + items: { + type: 'object', + properties: { + id: { type: 'string' }, + createdTime: { type: 'string' }, + fields: { type: 'object' }, + }, + }, + }, + metadata: { + type: 'json', + description: 'Operation metadata including pagination offset and total records count', + }, + }, } diff --git a/apps/sim/tools/airtable/update_multiple_records.ts b/apps/sim/tools/airtable/update_multiple_records.ts index 3c00190c0b..45c757de37 100644 --- a/apps/sim/tools/airtable/update_multiple_records.ts +++ b/apps/sim/tools/airtable/update_multiple_records.ts @@ -83,4 +83,23 @@ export const airtableUpdateMultipleRecordsTool: ToolConfig< // logger.error('Airtable tool error:', error) return `Failed to update multiple Airtable records: ${error.message || 'Unknown error'}` }, + + outputs: { + records: { + type: 'json', + description: 'Array of updated Airtable records', + items: { + type: 'object', + properties: { + id: { type: 'string' }, + createdTime: { type: 'string' }, + fields: { type: 'object' }, + }, + }, + }, + metadata: { + type: 'json', + description: 'Operation metadata including record count and updated record IDs', + }, + }, } diff --git a/apps/sim/tools/airtable/update_record.ts b/apps/sim/tools/airtable/update_record.ts index 3f17cbb75d..c455cca58f 100644 --- a/apps/sim/tools/airtable/update_record.ts +++ b/apps/sim/tools/airtable/update_record.ts @@ -44,9 +44,7 @@ export const airtableUpdateRecordTool: ToolConfig = { description: 'Search for academic papers on ArXiv by keywords, authors, titles, or other fields.', version: '1.0.0', + outputs: { + papers: { + type: 'json', + description: 'Array of papers matching the search query', + items: { + type: 'object', + properties: { + id: { type: 'string' }, + title: { type: 'string' }, + summary: { type: 'string' }, + authors: { type: 'string' }, + published: { type: 'string' }, + updated: { type: 'string' }, + link: { type: 'string' }, + pdfLink: { type: 'string' }, + categories: { type: 'string' }, + primaryCategory: { type: 'string' }, + comment: { type: 'string' }, + journalRef: { type: 'string' }, + doi: { type: 'string' }, + }, + }, + }, + totalResults: { + type: 'number', + description: 'Total number of results found for the search query', + }, + }, + params: { searchQuery: { type: 'string', diff --git a/apps/sim/tools/browser_use/run_task.ts b/apps/sim/tools/browser_use/run_task.ts index 8de6d20d23..578bad87e4 100644 --- a/apps/sim/tools/browser_use/run_task.ts +++ b/apps/sim/tools/browser_use/run_task.ts @@ -45,7 +45,15 @@ export const runTaskTool: ToolConfig params.webhookURL, method: 'POST', diff --git a/apps/sim/tools/confluence/retrieve.ts b/apps/sim/tools/confluence/retrieve.ts index 192aeaacca..a8415182cb 100644 --- a/apps/sim/tools/confluence/retrieve.ts +++ b/apps/sim/tools/confluence/retrieve.ts @@ -15,6 +15,13 @@ export const confluenceRetrieveTool: ToolConfig< provider: 'confluence', }, + outputs: { + ts: { type: 'string', description: 'Timestamp of retrieval' }, + pageId: { type: 'string', description: 'Confluence page ID' }, + content: { type: 'string', description: 'Page content with HTML tags stripped' }, + title: { type: 'string', description: 'Page title' }, + }, + params: { accessToken: { type: 'string', diff --git a/apps/sim/tools/confluence/update.ts b/apps/sim/tools/confluence/update.ts index d109cbb0fa..07bc5edaf9 100644 --- a/apps/sim/tools/confluence/update.ts +++ b/apps/sim/tools/confluence/update.ts @@ -12,6 +12,13 @@ export const confluenceUpdateTool: ToolConfig = { }, }, + outputs: { + answer: { + type: 'string', + description: 'AI-generated answer to the question', + }, + citations: { + type: 'array', + description: 'Sources and citations for the answer', + items: { + type: 'object', + properties: { + title: { type: 'string', description: 'The title of the cited source' }, + url: { type: 'string', description: 'The URL of the cited source' }, + text: { type: 'string', description: 'Relevant text from the cited source' }, + }, + }, + }, + }, + request: { url: 'https://api.exa.ai/answer', method: 'POST', diff --git a/apps/sim/tools/exa/find_similar_links.ts b/apps/sim/tools/exa/find_similar_links.ts index 45aafe8774..41d182d4a5 100644 --- a/apps/sim/tools/exa/find_similar_links.ts +++ b/apps/sim/tools/exa/find_similar_links.ts @@ -38,6 +38,28 @@ export const findSimilarLinksTool: ToolConfig< }, }, + outputs: { + similarLinks: { + type: 'array', + description: 'Similar links found with titles, URLs, and text snippets', + items: { + type: 'object', + properties: { + title: { type: 'string', description: 'The title of the similar webpage' }, + url: { type: 'string', description: 'The URL of the similar webpage' }, + text: { + type: 'string', + description: 'Text snippet or full content from the similar webpage', + }, + score: { + type: 'number', + description: 'Similarity score indicating how similar the page is', + }, + }, + }, + }, + }, + request: { url: 'https://api.exa.ai/findSimilar', method: 'POST', diff --git a/apps/sim/tools/exa/get_contents.ts b/apps/sim/tools/exa/get_contents.ts index 67c552dbb1..2d1e39e857 100644 --- a/apps/sim/tools/exa/get_contents.ts +++ b/apps/sim/tools/exa/get_contents.ts @@ -36,6 +36,22 @@ export const getContentsTool: ToolConfig = description: 'Exa AI API Key', }, }, + + outputs: { + research: { + type: 'array', + description: 'Comprehensive research findings with citations and summaries', + items: { + type: 'object', + properties: { + title: { type: 'string' }, + url: { type: 'string' }, + summary: { type: 'string' }, + text: { type: 'string' }, + publishedDate: { type: 'string' }, + author: { type: 'string' }, + score: { type: 'number' }, + }, + }, + }, + }, request: { url: 'https://api.exa.ai/research/v0/tasks', method: 'POST', diff --git a/apps/sim/tools/exa/search.ts b/apps/sim/tools/exa/search.ts index a27b00148b..38d9c62059 100644 --- a/apps/sim/tools/exa/search.ts +++ b/apps/sim/tools/exa/search.ts @@ -41,6 +41,27 @@ export const searchTool: ToolConfig = { }, }, + outputs: { + results: { + type: 'array', + description: 'Search results with titles, URLs, and text snippets', + items: { + type: 'object', + properties: { + title: { type: 'string', description: 'The title of the search result' }, + url: { type: 'string', description: 'The URL of the search result' }, + publishedDate: { type: 'string', description: 'Date when the content was published' }, + author: { type: 'string', description: 'The author of the content' }, + summary: { type: 'string', description: 'A brief summary of the content' }, + favicon: { type: 'string', description: "URL of the site's favicon" }, + image: { type: 'string', description: 'URL of a representative image from the page' }, + text: { type: 'string', description: 'Text snippet or full content from the page' }, + score: { type: 'number', description: 'Relevance score for the search result' }, + }, + }, + }, + }, + request: { url: 'https://api.exa.ai/search', method: 'POST', diff --git a/apps/sim/tools/firecrawl/crawl.ts b/apps/sim/tools/firecrawl/crawl.ts index 5c8f0c3e8e..3b18d8340b 100644 --- a/apps/sim/tools/firecrawl/crawl.ts +++ b/apps/sim/tools/firecrawl/crawl.ts @@ -153,4 +153,34 @@ export const crawlTool: ToolConfig } return error }, + + outputs: { + pages: { + type: 'array', + description: 'Array of crawled pages with their content and metadata', + items: { + type: 'object', + properties: { + markdown: { type: 'string', description: 'Page content in markdown format' }, + html: { type: 'string', description: 'Page HTML content' }, + metadata: { + type: 'object', + description: 'Page metadata', + properties: { + title: { type: 'string', description: 'Page title' }, + description: { type: 'string', description: 'Page description' }, + language: { type: 'string', description: 'Page language' }, + sourceURL: { type: 'string', description: 'Source URL of the page' }, + statusCode: { type: 'number', description: 'HTTP status code' }, + }, + }, + }, + }, + }, + total: { type: 'number', description: 'Total number of pages found during crawl' }, + creditsUsed: { + type: 'number', + description: 'Number of credits consumed by the crawl operation', + }, + }, } diff --git a/apps/sim/tools/firecrawl/scrape.ts b/apps/sim/tools/firecrawl/scrape.ts index 7d8496ba6a..2e92b8e007 100644 --- a/apps/sim/tools/firecrawl/scrape.ts +++ b/apps/sim/tools/firecrawl/scrape.ts @@ -64,4 +64,13 @@ export const scrapeTool: ToolConfig = { const code = error.error?.type || error.code return `${message} (${code})` }, + + outputs: { + markdown: { type: 'string', description: 'Page content in markdown format' }, + html: { type: 'string', description: 'Raw HTML content of the page' }, + metadata: { + type: 'object', + description: 'Page metadata including SEO and Open Graph information', + }, + }, } diff --git a/apps/sim/tools/firecrawl/search.ts b/apps/sim/tools/firecrawl/search.ts index cb5e47b0bd..574f17fbc3 100644 --- a/apps/sim/tools/firecrawl/search.ts +++ b/apps/sim/tools/firecrawl/search.ts @@ -55,4 +55,26 @@ export const searchTool: ToolConfig = { const code = error.error?.type || error.code return `${message} (${code})` }, + + outputs: { + data: { + type: 'array', + description: 'Search results data', + items: { + type: 'object', + properties: { + title: { type: 'string' }, + description: { type: 'string' }, + url: { type: 'string' }, + markdown: { type: 'string' }, + html: { type: 'string' }, + rawHtml: { type: 'string' }, + links: { type: 'array' }, + screenshot: { type: 'string' }, + metadata: { type: 'object' }, + }, + }, + }, + warning: { type: 'string', description: 'Warning messages from the search operation' }, + }, } diff --git a/apps/sim/tools/github/comment.ts b/apps/sim/tools/github/comment.ts index 2dbe2e3697..763c6c5363 100644 --- a/apps/sim/tools/github/comment.ts +++ b/apps/sim/tools/github/comment.ts @@ -134,4 +134,12 @@ export const commentTool: ToolConfig transformError: (error) => { return error instanceof Error ? error.message : 'Failed to create comment' }, + + outputs: { + content: { type: 'string', description: 'Human-readable comment confirmation' }, + metadata: { + type: 'object', + description: 'Comment metadata', + }, + }, } diff --git a/apps/sim/tools/github/latest_commit.ts b/apps/sim/tools/github/latest_commit.ts index bec7b0ded1..11d95c8fb9 100644 --- a/apps/sim/tools/github/latest_commit.ts +++ b/apps/sim/tools/github/latest_commit.ts @@ -144,4 +144,12 @@ export const latestCommitTool: ToolConfig { return error instanceof Error ? error.message : 'Failed to fetch PR details' }, + + outputs: { + content: { type: 'string', description: 'Human-readable PR summary' }, + metadata: { + type: 'object', + description: 'Detailed PR metadata including file changes', + properties: { + number: { type: 'number', description: 'Pull request number' }, + title: { type: 'string', description: 'PR title' }, + state: { type: 'string', description: 'PR state (open/closed/merged)' }, + html_url: { type: 'string', description: 'GitHub web URL' }, + diff_url: { type: 'string', description: 'Raw diff URL' }, + created_at: { type: 'string', description: 'Creation timestamp' }, + updated_at: { type: 'string', description: 'Last update timestamp' }, + files: { + type: 'array', + description: 'Files changed in the PR', + items: { + type: 'object', + properties: { + filename: { type: 'string', description: 'File path' }, + additions: { type: 'number', description: 'Lines added' }, + deletions: { type: 'number', description: 'Lines deleted' }, + changes: { type: 'number', description: 'Total changes' }, + patch: { type: 'string', description: 'File diff patch' }, + blob_url: { type: 'string', description: 'GitHub blob URL' }, + raw_url: { type: 'string', description: 'Raw file URL' }, + status: { type: 'string', description: 'Change type (added/modified/deleted)' }, + }, + }, + }, + }, + }, + }, } diff --git a/apps/sim/tools/github/repo_info.ts b/apps/sim/tools/github/repo_info.ts index b52dd8017a..830b1cea07 100644 --- a/apps/sim/tools/github/repo_info.ts +++ b/apps/sim/tools/github/repo_info.ts @@ -83,4 +83,20 @@ URL: ${data.html_url}` } return 'Failed to fetch repository information' }, + + outputs: { + content: { type: 'string', description: 'Human-readable repository summary' }, + metadata: { + type: 'object', + description: 'Repository metadata', + properties: { + name: { type: 'string', description: 'Repository name' }, + description: { type: 'string', description: 'Repository description' }, + stars: { type: 'number', description: 'Number of stars' }, + forks: { type: 'number', description: 'Number of forks' }, + openIssues: { type: 'number', description: 'Number of open issues' }, + language: { type: 'string', description: 'Primary programming language' }, + }, + }, + }, } diff --git a/apps/sim/tools/gmail/draft.ts b/apps/sim/tools/gmail/draft.ts index ec8455a435..375fe414f7 100644 --- a/apps/sim/tools/gmail/draft.ts +++ b/apps/sim/tools/gmail/draft.ts @@ -9,6 +9,26 @@ export const gmailDraftTool: ToolConfig = { description: 'Draft emails using Gmail', version: '1.0.0', + outputs: { + content: { type: 'string', description: 'Success message' }, + metadata: { + type: 'object', + description: 'Draft metadata', + properties: { + id: { type: 'string', description: 'Draft ID' }, + message: { + type: 'object', + description: 'Message metadata', + properties: { + id: { type: 'string', description: 'Gmail message ID' }, + threadId: { type: 'string', description: 'Gmail thread ID' }, + labelIds: { type: 'array', items: { type: 'string' }, description: 'Email labels' }, + }, + }, + }, + }, + }, + oauth: { required: true, provider: 'google-email', @@ -40,6 +60,18 @@ export const gmailDraftTool: ToolConfig = { visibility: 'user-or-llm', description: 'Email body content', }, + cc: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'CC recipients (comma-separated)', + }, + bcc: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'BCC recipients (comma-separated)', + }, }, request: { @@ -50,14 +82,21 @@ export const gmailDraftTool: ToolConfig = { 'Content-Type': 'application/json', }), body: (params: GmailSendParams): Record => { - const email = [ + const emailHeaders = [ 'Content-Type: text/plain; charset="UTF-8"', 'MIME-Version: 1.0', `To: ${params.to}`, - `Subject: ${params.subject}`, - '', - params.body, - ].join('\n') + ] + + if (params.cc) { + emailHeaders.push(`Cc: ${params.cc}`) + } + if (params.bcc) { + emailHeaders.push(`Bcc: ${params.bcc}`) + } + + emailHeaders.push(`Subject: ${params.subject}`, '', params.body) + const email = emailHeaders.join('\n') return { message: { diff --git a/apps/sim/tools/gmail/read.ts b/apps/sim/tools/gmail/read.ts index c2494a631a..54e334d3fa 100644 --- a/apps/sim/tools/gmail/read.ts +++ b/apps/sim/tools/gmail/read.ts @@ -1,4 +1,9 @@ -import type { GmailMessage, GmailReadParams, GmailToolResponse } from '@/tools/gmail/types' +import type { + GmailAttachment, + GmailMessage, + GmailReadParams, + GmailToolResponse, +} from '@/tools/gmail/types' import type { ToolConfig } from '@/tools/types' const GMAIL_API_BASE = 'https://gmail.googleapis.com/gmail/v1/users/me' @@ -12,7 +17,16 @@ export const gmailReadTool: ToolConfig = { oauth: { required: true, provider: 'google-email', - additionalScopes: ['https://www.googleapis.com/auth/gmail.labels'], + additionalScopes: [ + 'https://www.googleapis.com/auth/gmail.labels', + 'https://www.googleapis.com/auth/gmail.readonly', + ], + }, + + outputs: { + content: { type: 'string', description: 'Text content of the email' }, + metadata: { type: 'json', description: 'Metadata of the email' }, + attachments: { type: 'file[]', description: 'Attachments of the email' }, }, params: { @@ -46,6 +60,12 @@ export const gmailReadTool: ToolConfig = { visibility: 'user-only', description: 'Maximum number of messages to retrieve (default: 1, max: 10)', }, + includeAttachments: { + type: 'boolean', + required: false, + visibility: 'user-only', + description: 'Download and include email attachments', + }, }, request: { @@ -98,43 +118,77 @@ export const gmailReadTool: ToolConfig = { }, transformResponse: async (response: Response, params?: GmailReadParams) => { - try { - const data = await response.json() + const data = await response.json() - if (!response.ok) { - throw new Error(data.error?.message || 'Failed to read email') - } + if (!response.ok) { + throw new Error(data.error?.message || 'Failed to read email') + } + + // If we're fetching a single message directly (by ID) + if (params?.messageId) { + return await processMessage(data, params) + } - // If we're fetching a single message directly (by ID) - if (params?.messageId) { - return processMessage(data) + // If we're listing messages, we need to fetch each message's details + if (data.messages && Array.isArray(data.messages)) { + // Return a message if no emails found + if (data.messages.length === 0) { + return { + success: true, + output: { + content: 'No messages found in the selected folder.', + metadata: { + results: [], // Use SearchMetadata format + }, + }, + } } - // If we're listing messages, we need to fetch each message's details - if (data.messages && Array.isArray(data.messages)) { - // Return a message if no emails found - if (data.messages.length === 0) { + // For agentic workflows, we'll fetch the first message by default + // If maxResults > 1, we'll return a summary of messages found + const maxResults = params?.maxResults ? Math.min(params.maxResults, 10) : 1 + + if (maxResults === 1) { + try { + // Get the first message details + const messageId = data.messages[0].id + const messageResponse = await fetch( + `${GMAIL_API_BASE}/messages/${messageId}?format=full`, + { + headers: { + Authorization: `Bearer ${params?.accessToken || ''}`, + 'Content-Type': 'application/json', + }, + } + ) + + if (!messageResponse.ok) { + const errorData = await messageResponse.json() + throw new Error(errorData.error?.message || 'Failed to fetch message details') + } + + const message = await messageResponse.json() + return await processMessage(message, params) + } catch (error: any) { return { success: true, output: { - content: 'No messages found in the selected folder.', + content: `Found messages but couldn't retrieve details: ${error.message || 'Unknown error'}`, metadata: { - results: [], // Use SearchMetadata format + results: data.messages.map((msg: any) => ({ + id: msg.id, + threadId: msg.threadId, + })), }, }, } } - - // For agentic workflows, we'll fetch the first message by default - // If maxResults > 1, we'll return a summary of messages found - const maxResults = params?.maxResults ? Math.min(params.maxResults, 10) : 1 - - if (maxResults === 1) { - try { - // Get the first message details - const messageId = data.messages[0].id + } else { + // If maxResults > 1, fetch details for all messages + try { + const messagePromises = data.messages.slice(0, maxResults).map(async (msg: any) => { const messageResponse = await fetch( - `${GMAIL_API_BASE}/messages/${messageId}?format=full`, + `${GMAIL_API_BASE}/messages/${msg.id}?format=full`, { headers: { Authorization: `Bearer ${params?.accessToken || ''}`, @@ -144,99 +198,58 @@ export const gmailReadTool: ToolConfig = { ) if (!messageResponse.ok) { - const errorData = await messageResponse.json() - throw new Error(errorData.error?.message || 'Failed to fetch message details') + throw new Error(`Failed to fetch details for message ${msg.id}`) } - const message = await messageResponse.json() - return processMessage(message) - } catch (error: any) { - console.error('Error fetching message details:', error) - return { - success: true, - output: { - content: `Found messages but couldn't retrieve details: ${error.message || 'Unknown error'}`, - metadata: { - results: data.messages.map((msg: any) => ({ - id: msg.id, - threadId: msg.threadId, - })), - }, - }, - } - } - } else { - // If maxResults > 1, fetch details for all messages - try { - const messagePromises = data.messages.slice(0, maxResults).map(async (msg: any) => { - const messageResponse = await fetch( - `${GMAIL_API_BASE}/messages/${msg.id}?format=full`, - { - headers: { - Authorization: `Bearer ${params?.accessToken || ''}`, - 'Content-Type': 'application/json', - }, - } - ) - - if (!messageResponse.ok) { - throw new Error(`Failed to fetch details for message ${msg.id}`) - } + return await messageResponse.json() + }) - return await messageResponse.json() - }) - - const messages = await Promise.all(messagePromises) - - // Process all messages and create a summary - const processedMessages = messages.map(processMessageForSummary) - - return { - success: true, - output: { - content: createMessagesSummary(processedMessages), - metadata: { - results: processedMessages.map((msg) => ({ - id: msg.id, - threadId: msg.threadId, - subject: msg.subject, - from: msg.from, - date: msg.date, - })), - }, + const messages = await Promise.all(messagePromises) + + // Process all messages and create a summary + const processedMessages = messages.map(processMessageForSummary) + + return { + success: true, + output: { + content: createMessagesSummary(processedMessages), + metadata: { + results: processedMessages.map((msg) => ({ + id: msg.id, + threadId: msg.threadId, + subject: msg.subject, + from: msg.from, + date: msg.date, + })), }, - } - } catch (error: any) { - console.error('Error fetching multiple message details:', error) - return { - success: true, - output: { - content: `Found ${data.messages.length} messages but couldn't retrieve all details: ${error.message || 'Unknown error'}`, - metadata: { - results: data.messages.map((msg: any) => ({ - id: msg.id, - threadId: msg.threadId, - })), - }, + }, + } + } catch (error: any) { + return { + success: true, + output: { + content: `Found ${data.messages.length} messages but couldn't retrieve all details: ${error.message || 'Unknown error'}`, + metadata: { + results: data.messages.map((msg: any) => ({ + id: msg.id, + threadId: msg.threadId, + })), }, - } + }, } } } + } - // Fallback for unexpected response format - return { - success: true, - output: { - content: 'Unexpected response format from Gmail API', - metadata: { - results: [], - }, + // Fallback for unexpected response format + return { + success: true, + output: { + content: 'Unexpected response format from Gmail API', + metadata: { + results: [], }, - } - } catch (error) { - console.error('Error in transformResponse:', error) - throw error + }, } }, @@ -255,7 +268,10 @@ export const gmailReadTool: ToolConfig = { } // Helper function to process a Gmail message -function processMessage(message: GmailMessage): GmailToolResponse { +async function processMessage( + message: GmailMessage, + params?: GmailReadParams +): Promise { // Check if message and payload exist if (!message || !message.payload) { return { @@ -280,7 +296,21 @@ function processMessage(message: GmailMessage): GmailToolResponse { // Extract the message body const body = extractMessageBody(message.payload) - return { + // Check for attachments + const attachmentInfo = extractAttachmentInfo(message.payload) + const hasAttachments = attachmentInfo.length > 0 + + // Download attachments if requested + let attachments: GmailAttachment[] | undefined + if (params?.includeAttachments && hasAttachments && params.accessToken) { + try { + attachments = await downloadAttachments(message.id, attachmentInfo, params.accessToken) + } catch (error) { + // Continue without attachments rather than failing the entire request + } + } + + const result: GmailToolResponse = { success: true, output: { content: body || 'No content found in email', @@ -292,9 +322,15 @@ function processMessage(message: GmailMessage): GmailToolResponse { to, subject, date, + hasAttachments, + attachmentCount: attachmentInfo.length, }, + // Always include attachments array (empty if none downloaded) + attachments: attachments || [], }, } + + return result } // Helper function to process a message for summary (without full content) @@ -382,3 +418,83 @@ function extractMessageBody(payload: any): string { // If we couldn't find any text content, return empty string return '' } + +// Helper function to extract attachment information from message payload +function extractAttachmentInfo( + payload: any +): Array<{ attachmentId: string; filename: string; mimeType: string; size: number }> { + const attachments: Array<{ + attachmentId: string + filename: string + mimeType: string + size: number + }> = [] + + function processPayloadPart(part: any) { + // Check if this part has an attachment + if (part.body?.attachmentId && part.filename) { + attachments.push({ + attachmentId: part.body.attachmentId, + filename: part.filename, + mimeType: part.mimeType || 'application/octet-stream', + size: part.body.size || 0, + }) + } + + // Recursively process nested parts + if (part.parts && Array.isArray(part.parts)) { + part.parts.forEach(processPayloadPart) + } + } + + // Process the main payload + processPayloadPart(payload) + + return attachments +} + +// Helper function to download attachments from Gmail API +async function downloadAttachments( + messageId: string, + attachmentInfo: Array<{ attachmentId: string; filename: string; mimeType: string; size: number }>, + accessToken: string +): Promise { + const downloadedAttachments: GmailAttachment[] = [] + + for (const attachment of attachmentInfo) { + try { + // Download attachment from Gmail API + const attachmentResponse = await fetch( + `${GMAIL_API_BASE}/messages/${messageId}/attachments/${attachment.attachmentId}`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, + } + ) + + if (!attachmentResponse.ok) { + continue + } + + const attachmentData = (await attachmentResponse.json()) as { data: string; size: number } + + // Decode base64url data to buffer + // Gmail API returns data in base64url format (URL-safe base64) + const base64Data = attachmentData.data.replace(/-/g, '+').replace(/_/g, '/') + const buffer = Buffer.from(base64Data, 'base64') + + downloadedAttachments.push({ + name: attachment.filename, + data: buffer, + mimeType: attachment.mimeType, + size: attachment.size, + }) + } catch (error) { + // Continue with other attachments + } + } + + return downloadedAttachments +} diff --git a/apps/sim/tools/gmail/search.ts b/apps/sim/tools/gmail/search.ts index 78c0130b91..40b3dbab5b 100644 --- a/apps/sim/tools/gmail/search.ts +++ b/apps/sim/tools/gmail/search.ts @@ -9,6 +9,31 @@ export const gmailSearchTool: ToolConfig = description: 'Search emails in Gmail', version: '1.0.0', + outputs: { + content: { type: 'string', description: 'Search results summary' }, + metadata: { + type: 'object', + description: 'Search metadata', + properties: { + results: { + type: 'array', + description: 'Array of search results', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Gmail message ID' }, + threadId: { type: 'string', description: 'Gmail thread ID' }, + subject: { type: 'string', description: 'Email subject' }, + from: { type: 'string', description: 'Sender email address' }, + date: { type: 'string', description: 'Email date' }, + snippet: { type: 'string', description: 'Email snippet/preview' }, + }, + }, + }, + }, + }, + }, + oauth: { required: true, provider: 'google-email', diff --git a/apps/sim/tools/gmail/send.ts b/apps/sim/tools/gmail/send.ts index ed18f9ecbf..8d5b3741b8 100644 --- a/apps/sim/tools/gmail/send.ts +++ b/apps/sim/tools/gmail/send.ts @@ -9,6 +9,19 @@ export const gmailSendTool: ToolConfig = { description: 'Send emails using Gmail', version: '1.0.0', + outputs: { + content: { type: 'string', description: 'Success message' }, + metadata: { + type: 'object', + description: 'Email metadata', + properties: { + id: { type: 'string', description: 'Gmail message ID' }, + threadId: { type: 'string', description: 'Gmail thread ID' }, + labelIds: { type: 'array', items: { type: 'string' }, description: 'Email labels' }, + }, + }, + }, + oauth: { required: true, provider: 'google-email', @@ -40,6 +53,18 @@ export const gmailSendTool: ToolConfig = { visibility: 'user-or-llm', description: 'Email body content', }, + cc: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'CC recipients (comma-separated)', + }, + bcc: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'BCC recipients (comma-separated)', + }, }, request: { @@ -50,14 +75,21 @@ export const gmailSendTool: ToolConfig = { 'Content-Type': 'application/json', }), body: (params: GmailSendParams): Record => { - const email = [ + const emailHeaders = [ 'Content-Type: text/plain; charset="UTF-8"', 'MIME-Version: 1.0', `To: ${params.to}`, - `Subject: ${params.subject}`, - '', - params.body, - ].join('\n') + ] + + if (params.cc) { + emailHeaders.push(`Cc: ${params.cc}`) + } + if (params.bcc) { + emailHeaders.push(`Bcc: ${params.bcc}`) + } + + emailHeaders.push(`Subject: ${params.subject}`, '', params.body) + const email = emailHeaders.join('\n') return { raw: Buffer.from(email).toString('base64url'), diff --git a/apps/sim/tools/gmail/types.ts b/apps/sim/tools/gmail/types.ts index 9449701071..4115f7ea2e 100644 --- a/apps/sim/tools/gmail/types.ts +++ b/apps/sim/tools/gmail/types.ts @@ -8,6 +8,8 @@ interface BaseGmailParams { // Send operation parameters export interface GmailSendParams extends BaseGmailParams { to: string + cc?: string + bcc?: string subject: string body: string } @@ -18,6 +20,7 @@ export interface GmailReadParams extends BaseGmailParams { folder: string unreadOnly?: boolean maxResults?: number + includeAttachments?: boolean } // Search operation parameters @@ -41,6 +44,8 @@ interface EmailMetadata extends BaseGmailMetadata { to?: string subject?: string date?: string + hasAttachments?: boolean + attachmentCount?: number } interface SearchMetadata extends BaseGmailMetadata { @@ -55,6 +60,7 @@ export interface GmailToolResponse extends ToolResponse { output: { content: string metadata: EmailMetadata | SearchMetadata + attachments?: GmailAttachment[] } } @@ -71,12 +77,26 @@ export interface GmailMessage { }> body: { data?: string + attachmentId?: string + size?: number } parts?: Array<{ mimeType: string + filename?: string body: { data?: string + attachmentId?: string + size?: number } + parts?: Array }> } } + +// Gmail Attachment Interface (for processed attachments) +export interface GmailAttachment { + name: string + data: Buffer + mimeType: string + size: number +} diff --git a/apps/sim/tools/google/search.ts b/apps/sim/tools/google/search.ts index 7a870df840..b903a76dfd 100644 --- a/apps/sim/tools/google/search.ts +++ b/apps/sim/tools/google/search.ts @@ -34,6 +34,36 @@ export const searchTool: ToolConfig = }, }, + outputs: { + items: { + type: 'array', + description: 'Array of search results from Google', + items: { + type: 'object', + properties: { + title: { type: 'string', description: 'Title of the search result' }, + link: { type: 'string', description: 'URL of the search result' }, + snippet: { type: 'string', description: 'Snippet or description of the search result' }, + displayLink: { type: 'string', description: 'Display URL', optional: true }, + pagemap: { type: 'object', description: 'Additional page metadata', optional: true }, + }, + }, + }, + searchInformation: { + type: 'object', + description: 'Information about the search query and results', + properties: { + totalResults: { type: 'string', description: 'Total number of search results available' }, + searchTime: { type: 'number', description: 'Time taken to perform the search in seconds' }, + formattedSearchTime: { type: 'string', description: 'Formatted search time for display' }, + formattedTotalResults: { + type: 'string', + description: 'Formatted total results count for display', + }, + }, + }, + }, + request: { url: (params: GoogleSearchParams) => { const baseUrl = 'https://www.googleapis.com/customsearch/v1' diff --git a/apps/sim/tools/google_calendar/create.ts b/apps/sim/tools/google_calendar/create.ts index fe1f7d09d4..0395bdcfc7 100644 --- a/apps/sim/tools/google_calendar/create.ts +++ b/apps/sim/tools/google_calendar/create.ts @@ -148,6 +148,14 @@ export const createTool: ToolConfig { if (!response.ok) { const errorData = await response.json() diff --git a/apps/sim/tools/google_calendar/get.ts b/apps/sim/tools/google_calendar/get.ts index da02234348..e8f218debc 100644 --- a/apps/sim/tools/google_calendar/get.ts +++ b/apps/sim/tools/google_calendar/get.ts @@ -51,6 +51,14 @@ export const getTool: ToolConfig { if (!response.ok) { const errorData = await response.json() diff --git a/apps/sim/tools/google_calendar/invite.ts b/apps/sim/tools/google_calendar/invite.ts index 08891c531a..dcabce4970 100644 --- a/apps/sim/tools/google_calendar/invite.ts +++ b/apps/sim/tools/google_calendar/invite.ts @@ -56,6 +56,17 @@ export const inviteTool: ToolConfig { const calendarId = params.calendarId || 'primary' diff --git a/apps/sim/tools/google_calendar/list.ts b/apps/sim/tools/google_calendar/list.ts index c4093efba3..55bee562c1 100644 --- a/apps/sim/tools/google_calendar/list.ts +++ b/apps/sim/tools/google_calendar/list.ts @@ -80,6 +80,14 @@ export const listTool: ToolConfig { if (!response.ok) { const errorData = await response.json() diff --git a/apps/sim/tools/google_calendar/quick_add.ts b/apps/sim/tools/google_calendar/quick_add.ts index fa2c62beda..b4a078d20e 100644 --- a/apps/sim/tools/google_calendar/quick_add.ts +++ b/apps/sim/tools/google_calendar/quick_add.ts @@ -74,6 +74,14 @@ export const quickAddTool: ToolConfig< }), }, + outputs: { + content: { + type: 'string', + description: 'Event creation confirmation message from natural language', + }, + metadata: { type: 'json', description: 'Created event metadata including parsed details' }, + }, + transformResponse: async (response: Response, params) => { const data = await response.json() diff --git a/apps/sim/tools/google_calendar/update.ts b/apps/sim/tools/google_calendar/update.ts index ea2a8bd3b5..2cef6d90e2 100644 --- a/apps/sim/tools/google_calendar/update.ts +++ b/apps/sim/tools/google_calendar/update.ts @@ -86,6 +86,14 @@ export const updateTool: ToolConfig { const calendarId = params.calendarId || 'primary' diff --git a/apps/sim/tools/google_docs/create.ts b/apps/sim/tools/google_docs/create.ts index 1b6f6fae99..e748d702b4 100644 --- a/apps/sim/tools/google_docs/create.ts +++ b/apps/sim/tools/google_docs/create.ts @@ -112,6 +112,14 @@ export const createTool: ToolConfig { if (!response.ok) { let errorText = '' diff --git a/apps/sim/tools/google_docs/read.ts b/apps/sim/tools/google_docs/read.ts index 02bfbc3d95..53057a7bb9 100644 --- a/apps/sim/tools/google_docs/read.ts +++ b/apps/sim/tools/google_docs/read.ts @@ -47,6 +47,12 @@ export const readTool: ToolConfig } }, }, + + outputs: { + content: { type: 'string', description: 'Extracted document text content' }, + metadata: { type: 'json', description: 'Document metadata including ID, title, and URL' }, + }, + transformResponse: async (response: Response) => { if (!response.ok) { const errorText = await response.text() diff --git a/apps/sim/tools/google_docs/write.ts b/apps/sim/tools/google_docs/write.ts index 33fa654b2b..b92760a22e 100644 --- a/apps/sim/tools/google_docs/write.ts +++ b/apps/sim/tools/google_docs/write.ts @@ -74,6 +74,18 @@ export const writeTool: ToolConfig { if (!response.ok) { let errorText = '' diff --git a/apps/sim/tools/google_drive/create_folder.ts b/apps/sim/tools/google_drive/create_folder.ts index fca7624cea..30dd5f9c62 100644 --- a/apps/sim/tools/google_drive/create_folder.ts +++ b/apps/sim/tools/google_drive/create_folder.ts @@ -63,6 +63,14 @@ export const createFolderTool: ToolConfig { if (!response.ok) { const data = await response.json().catch(() => ({})) diff --git a/apps/sim/tools/google_drive/get_content.ts b/apps/sim/tools/google_drive/get_content.ts index 4d188fa0ba..81525e6eb3 100644 --- a/apps/sim/tools/google_drive/get_content.ts +++ b/apps/sim/tools/google_drive/get_content.ts @@ -39,6 +39,18 @@ export const getContentTool: ToolConfig `https://www.googleapis.com/drive/v3/files/${params.fileId}?fields=id,name,mimeType`, diff --git a/apps/sim/tools/google_drive/list.ts b/apps/sim/tools/google_drive/list.ts index 70a138a970..866937b13f 100644 --- a/apps/sim/tools/google_drive/list.ts +++ b/apps/sim/tools/google_drive/list.ts @@ -86,6 +86,14 @@ export const listTool: ToolConfig { const data = await response.json() diff --git a/apps/sim/tools/google_drive/upload.ts b/apps/sim/tools/google_drive/upload.ts index ab66575ddb..496bd4db7b 100644 --- a/apps/sim/tools/google_drive/upload.ts +++ b/apps/sim/tools/google_drive/upload.ts @@ -79,6 +79,11 @@ export const uploadTool: ToolConfig { try { const data = await response.json() diff --git a/apps/sim/tools/google_sheets/append.ts b/apps/sim/tools/google_sheets/append.ts index dbd92860cf..3f9735cbbc 100644 --- a/apps/sim/tools/google_sheets/append.ts +++ b/apps/sim/tools/google_sheets/append.ts @@ -176,6 +176,16 @@ export const appendTool: ToolConfig { if (!response.ok) { const errorText = await response.text() diff --git a/apps/sim/tools/google_sheets/read.ts b/apps/sim/tools/google_sheets/read.ts index b8a8c236c3..074eb5b449 100644 --- a/apps/sim/tools/google_sheets/read.ts +++ b/apps/sim/tools/google_sheets/read.ts @@ -59,6 +59,12 @@ export const readTool: ToolConfig { if (!response.ok) { const errorJson = await response.json().catch(() => ({ error: response.statusText })) diff --git a/apps/sim/tools/google_sheets/update.ts b/apps/sim/tools/google_sheets/update.ts index c11766a7c6..8161d9d82c 100644 --- a/apps/sim/tools/google_sheets/update.ts +++ b/apps/sim/tools/google_sheets/update.ts @@ -130,6 +130,15 @@ export const updateTool: ToolConfig { if (!response.ok) { const errorText = await response.text() diff --git a/apps/sim/tools/google_sheets/write.ts b/apps/sim/tools/google_sheets/write.ts index 66fe15db28..3185bba65d 100644 --- a/apps/sim/tools/google_sheets/write.ts +++ b/apps/sim/tools/google_sheets/write.ts @@ -127,6 +127,15 @@ export const writeTool: ToolConfig { if (!response.ok) { const errorText = await response.text() diff --git a/apps/sim/tools/http/request.ts b/apps/sim/tools/http/request.ts index 4c0d7c870d..b19793120f 100644 --- a/apps/sim/tools/http/request.ts +++ b/apps/sim/tools/http/request.ts @@ -221,6 +221,29 @@ export const requestTool: ToolConfig = { }, }, + outputs: { + data: { + type: 'json', + description: 'Response data from the HTTP request (JSON object, text, or other format)', + }, + status: { + type: 'number', + description: 'HTTP status code of the response (e.g., 200, 404, 500)', + }, + headers: { + type: 'object', + description: 'Response headers as key-value pairs', + properties: { + 'content-type': { + type: 'string', + description: 'Content type of the response', + optional: true, + }, + 'content-length': { type: 'string', description: 'Content length', optional: true }, + }, + }, + }, + // Direct execution to bypass server for HTTP requests directExecution: async (params: RequestParams): Promise => { try { diff --git a/apps/sim/tools/huggingface/chat.ts b/apps/sim/tools/huggingface/chat.ts index 82933c7ef3..1fcccd18b6 100644 --- a/apps/sim/tools/huggingface/chat.ts +++ b/apps/sim/tools/huggingface/chat.ts @@ -56,7 +56,29 @@ export const chatTool: ToolConfig { diff --git a/apps/sim/tools/hunter/companies_find.ts b/apps/sim/tools/hunter/companies_find.ts index e25c54e12e..ac1ac46d7f 100644 --- a/apps/sim/tools/hunter/companies_find.ts +++ b/apps/sim/tools/hunter/companies_find.ts @@ -22,6 +22,18 @@ export const companiesFindTool: ToolConfig { const url = new URL('https://api.hunter.io/v2/companies/find') diff --git a/apps/sim/tools/hunter/discover.ts b/apps/sim/tools/hunter/discover.ts index 0c037d1de6..6b07500bfd 100644 --- a/apps/sim/tools/hunter/discover.ts +++ b/apps/sim/tools/hunter/discover.ts @@ -46,6 +46,14 @@ export const discoverTool: ToolConfig { // Validate that at least one search parameter is provided diff --git a/apps/sim/tools/hunter/domain_search.ts b/apps/sim/tools/hunter/domain_search.ts index 79eec15ea7..ed9a766aa8 100644 --- a/apps/sim/tools/hunter/domain_search.ts +++ b/apps/sim/tools/hunter/domain_search.ts @@ -52,6 +52,90 @@ export const domainSearchTool: ToolConfig { const url = new URL('https://api.hunter.io/v2/domain-search') diff --git a/apps/sim/tools/hunter/email_count.ts b/apps/sim/tools/hunter/email_count.ts index 6658cbd679..f5feb5fe05 100644 --- a/apps/sim/tools/hunter/email_count.ts +++ b/apps/sim/tools/hunter/email_count.ts @@ -34,6 +34,30 @@ export const emailCountTool: ToolConfig { if (!params.domain && !params.company) { diff --git a/apps/sim/tools/hunter/email_finder.ts b/apps/sim/tools/hunter/email_finder.ts index 2ec48dfdb8..5a8c5c7b6e 100644 --- a/apps/sim/tools/hunter/email_finder.ts +++ b/apps/sim/tools/hunter/email_finder.ts @@ -41,6 +41,26 @@ export const emailFinderTool: ToolConfig { const url = new URL('https://api.hunter.io/v2/email-finder') diff --git a/apps/sim/tools/hunter/email_verifier.ts b/apps/sim/tools/hunter/email_verifier.ts index 3ff449cfd2..7115dfa3e8 100644 --- a/apps/sim/tools/hunter/email_verifier.ts +++ b/apps/sim/tools/hunter/email_verifier.ts @@ -24,6 +24,66 @@ export const emailVerifierTool: ToolConfig { const url = new URL('https://api.hunter.io/v2/email-verifier') diff --git a/apps/sim/tools/index.ts b/apps/sim/tools/index.ts index cb306772e9..eead275ac7 100644 --- a/apps/sim/tools/index.ts +++ b/apps/sim/tools/index.ts @@ -1,5 +1,6 @@ import { createLogger } from '@/lib/logs/console/logger' import { getBaseUrl } from '@/lib/urls/utils' +import type { ExecutionContext } from '@/executor/types' import type { OAuthTokenPayload, ToolConfig, ToolResponse } from '@/tools/types' import { formatRequestParams, @@ -10,12 +11,63 @@ import { const logger = createLogger('Tools') +/** + * Process file outputs for a tool result if execution context is available + * Uses dynamic imports to avoid client-side bundling issues + */ +async function processFileOutputs( + result: ToolResponse, + tool: ToolConfig, + executionContext?: ExecutionContext +): Promise { + // Skip file processing if no execution context or not successful + if (!executionContext || !result.success) { + return result + } + + // Skip file processing on client-side (no Node.js modules available) + if (typeof window !== 'undefined') { + return result + } + + try { + // Dynamic import to avoid client-side bundling issues + const { FileToolProcessor } = await import('@/executor/utils/file-tool-processor') + + // Check if tool has file outputs + if (!FileToolProcessor.hasFileOutputs(tool)) { + return result + } + + logger.info(`File processing for tool ${tool.id}: checking outputs`, Object.keys(result.output)) + const processedOutput = await FileToolProcessor.processToolOutputs( + result.output, + tool, + executionContext + ) + logger.info( + `File processing for tool ${tool.id}: processed outputs`, + Object.keys(processedOutput) + ) + + return { + ...result, + output: processedOutput, + } + } catch (error) { + logger.error(`Error processing file outputs for tool ${tool.id}:`, error) + // Return original result if file processing fails + return result + } +} + // Execute a tool by calling either the proxy for external APIs or directly for internal routes export async function executeTool( toolId: string, params: Record, skipProxy = false, - skipPostProcess = false + skipPostProcess = false, + executionContext?: ExecutionContext ): Promise { // Capture start time for precise timing const startTime = new Date() @@ -124,38 +176,23 @@ export async function executeTool( const duration = endTime.getTime() - startTime.getTime() // Apply post-processing if available and not skipped + let finalResult = directResult if (tool.postProcess && directResult.success && !skipPostProcess) { try { - const postProcessResult = await tool.postProcess( - directResult, - contextParams, - executeTool - ) - return { - ...postProcessResult, - timing: { - startTime: startTimeISO, - endTime: endTimeISO, - duration, - }, - } + finalResult = await tool.postProcess(directResult, contextParams, executeTool) } catch (error) { logger.error(`[${requestId}] Post-processing error for ${toolId}:`, { error: error instanceof Error ? error.message : String(error), }) - return { - ...directResult, - timing: { - startTime: startTimeISO, - endTime: endTimeISO, - duration, - }, - } + finalResult = directResult } } + // Process file outputs if execution context is available + finalResult = await processFileOutputs(finalResult, tool, executionContext) + return { - ...directResult, + ...finalResult, timing: { startTime: startTimeISO, endTime: endTimeISO, @@ -177,48 +214,27 @@ export async function executeTool( const result = await handleInternalRequest(toolId, tool, contextParams) // Apply post-processing if available and not skipped + let finalResult = result if (tool.postProcess && result.success && !skipPostProcess) { try { - const postProcessResult = await tool.postProcess(result, contextParams, executeTool) - - // Add timing data to the post-processed result - const endTime = new Date() - const endTimeISO = endTime.toISOString() - const duration = endTime.getTime() - startTime.getTime() - return { - ...postProcessResult, - timing: { - startTime: startTimeISO, - endTime: endTimeISO, - duration, - }, - } + finalResult = await tool.postProcess(result, contextParams, executeTool) } catch (error) { logger.error(`[${requestId}] Post-processing error for ${toolId}:`, { error: error instanceof Error ? error.message : String(error), }) - // Return original result if post-processing fails - // Still include timing data - const endTime = new Date() - const endTimeISO = endTime.toISOString() - const duration = endTime.getTime() - startTime.getTime() - return { - ...result, - timing: { - startTime: startTimeISO, - endTime: endTimeISO, - duration, - }, - } + finalResult = result } } + // Process file outputs if execution context is available + finalResult = await processFileOutputs(finalResult, tool, executionContext) + // Add timing data to the result const endTime = new Date() const endTimeISO = endTime.toISOString() const duration = endTime.getTime() - startTime.getTime() return { - ...result, + ...finalResult, timing: { startTime: startTimeISO, endTime: endTimeISO, @@ -228,50 +244,30 @@ export async function executeTool( } // For external APIs, use the proxy - const result = await handleProxyRequest(toolId, contextParams) + const result = await handleProxyRequest(toolId, contextParams, executionContext) // Apply post-processing if available and not skipped + let finalResult = result if (tool.postProcess && result.success && !skipPostProcess) { try { - const postProcessResult = await tool.postProcess(result, contextParams, executeTool) - - // Add timing data to the post-processed result - const endTime = new Date() - const endTimeISO = endTime.toISOString() - const duration = endTime.getTime() - startTime.getTime() - return { - ...postProcessResult, - timing: { - startTime: startTimeISO, - endTime: endTimeISO, - duration, - }, - } + finalResult = await tool.postProcess(result, contextParams, executeTool) } catch (error) { logger.error(`[${requestId}] Post-processing error for ${toolId}:`, { error: error instanceof Error ? error.message : String(error), }) - // Return original result if post-processing fails, but include timing data - const endTime = new Date() - const endTimeISO = endTime.toISOString() - const duration = endTime.getTime() - startTime.getTime() - return { - ...result, - timing: { - startTime: startTimeISO, - endTime: endTimeISO, - duration, - }, - } + finalResult = result } } + // Process file outputs if execution context is available + finalResult = await processFileOutputs(finalResult, tool, executionContext) + // Add timing data to the result const endTime = new Date() const endTimeISO = endTime.toISOString() const duration = endTime.getTime() - startTime.getTime() return { - ...result, + ...finalResult, timing: { startTime: startTimeISO, endTime: endTimeISO, @@ -592,7 +588,8 @@ function validateClientSideParams( */ async function handleProxyRequest( toolId: string, - params: Record + params: Record, + executionContext?: ExecutionContext ): Promise { const requestId = crypto.randomUUID().slice(0, 8) @@ -603,7 +600,7 @@ async function handleProxyRequest( const response = await fetch(proxyUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ toolId, params }), + body: JSON.stringify({ toolId, params, executionContext }), }) if (!response.ok) { diff --git a/apps/sim/tools/jina/read_url.ts b/apps/sim/tools/jina/read_url.ts index 8b21699611..88287165d0 100644 --- a/apps/sim/tools/jina/read_url.ts +++ b/apps/sim/tools/jina/read_url.ts @@ -41,6 +41,13 @@ export const readUrlTool: ToolConfig = { }, }, + outputs: { + content: { + type: 'string', + description: 'The extracted content from the URL, processed into clean, LLM-friendly text', + }, + }, + request: { url: (params: ReadUrlParams) => { return `https://r.jina.ai/https://${params.url.replace(/^https?:\/\//, '')}` diff --git a/apps/sim/tools/jira/bulk_read.ts b/apps/sim/tools/jira/bulk_read.ts index 09622a531a..5a882542f5 100644 --- a/apps/sim/tools/jira/bulk_read.ts +++ b/apps/sim/tools/jira/bulk_read.ts @@ -37,6 +37,16 @@ export const jiraBulkRetrieveTool: ToolConfig { if (params.cloudId) { diff --git a/apps/sim/tools/jira/retrieve.ts b/apps/sim/tools/jira/retrieve.ts index cb18626cbb..92b54df7d2 100644 --- a/apps/sim/tools/jira/retrieve.ts +++ b/apps/sim/tools/jira/retrieve.ts @@ -46,6 +46,17 @@ export const jiraRetrieveTool: ToolConfig { diff --git a/apps/sim/tools/jira/update.ts b/apps/sim/tools/jira/update.ts index 607ee26a47..098c209687 100644 --- a/apps/sim/tools/jira/update.ts +++ b/apps/sim/tools/jira/update.ts @@ -78,6 +78,17 @@ export const jiraUpdateTool: ToolConfig = 'Jira Cloud ID for the instance. If not provided, it will be fetched using the domain.', }, }, + outputs: { + success: { + type: 'boolean', + description: 'Operation success status', + }, + output: { + type: 'object', + description: + 'Updated Jira issue details with timestamp, issue key, summary, and success status', + }, + }, directExecution: async (params) => { // Pre-fetch the cloudId if not provided diff --git a/apps/sim/tools/jira/write.ts b/apps/sim/tools/jira/write.ts index 8206a274d5..c0cff2ef80 100644 --- a/apps/sim/tools/jira/write.ts +++ b/apps/sim/tools/jira/write.ts @@ -81,6 +81,17 @@ export const jiraWriteTool: ToolConfig = { description: 'Type of issue to create (e.g., Task, Story)', }, }, + outputs: { + success: { + type: 'boolean', + description: 'Operation success status', + }, + output: { + type: 'object', + description: + 'Created Jira issue details with timestamp, issue key, summary, success status, and URL', + }, + }, directExecution: async (params) => { // Pre-fetch the cloudId if not provided diff --git a/apps/sim/tools/knowledge/create_document.ts b/apps/sim/tools/knowledge/create_document.ts index 6a6bbef588..0e48cc2805 100644 --- a/apps/sim/tools/knowledge/create_document.ts +++ b/apps/sim/tools/knowledge/create_document.ts @@ -63,6 +63,30 @@ export const knowledgeCreateDocumentTool: ToolConfig `/api/knowledge/${params.knowledgeBaseId}/documents`, method: 'POST', @@ -83,8 +107,8 @@ export const knowledgeCreateDocumentTool: ToolConfig:"/\\|?*]/.test(documentName)) { throw new Error('Document name contains invalid characters. Avoid: < > : " / \\ | ? *') } - if (!textContent || textContent.length < 10) { - throw new Error('Document content must be at least 10 characters long') + if (!textContent || textContent.length < 1) { + throw new Error('Document content cannot be empty') } if (textContent.length > 1000000) { throw new Error('Document content exceeds maximum size of 1MB') @@ -133,7 +157,7 @@ export const knowledgeCreateDocumentTool: ToolConfig = { description: 'Array of tag filters with tagName and tagValue properties', }, }, + + outputs: { + results: { + type: 'array', + description: 'Array of search results from the knowledge base', + items: { + type: 'object', + properties: { + id: { type: 'string' }, + content: { type: 'string' }, + documentId: { type: 'string' }, + chunkIndex: { type: 'number' }, + similarity: { type: 'number' }, + metadata: { type: 'object' }, + }, + }, + }, + query: { + type: 'string', + description: 'The search query that was executed', + }, + totalResults: { + type: 'number', + description: 'Total number of results found', + }, + cost: { + type: 'object', + description: 'Cost information for the search operation', + optional: true, + }, + }, + request: { url: () => '/api/knowledge/search', method: 'POST', diff --git a/apps/sim/tools/knowledge/upload_chunk.ts b/apps/sim/tools/knowledge/upload_chunk.ts index 9a1f880a2f..7e1f055f5f 100644 --- a/apps/sim/tools/knowledge/upload_chunk.ts +++ b/apps/sim/tools/knowledge/upload_chunk.ts @@ -23,6 +23,37 @@ export const knowledgeUploadChunkTool: ToolConfig `/api/knowledge/${params.knowledgeBaseId}/documents/${params.documentId}/chunks`, diff --git a/apps/sim/tools/linear/create_issue.ts b/apps/sim/tools/linear/create_issue.ts index 8c5eb793f0..27f46aa6fa 100644 --- a/apps/sim/tools/linear/create_issue.ts +++ b/apps/sim/tools/linear/create_issue.ts @@ -37,6 +37,22 @@ export const linearCreateIssueTool: ToolConfig) => { // For a specific memory ID, use the get single memory endpoint diff --git a/apps/sim/tools/mem0/search_memories.ts b/apps/sim/tools/mem0/search_memories.ts index b91ba9e6b2..654454cd16 100644 --- a/apps/sim/tools/mem0/search_memories.ts +++ b/apps/sim/tools/mem0/search_memories.ts @@ -34,6 +34,17 @@ export const mem0SearchMemoriesTool: ToolConfig = { description: 'Your Mem0 API key', }, }, + + outputs: { + searchResults: { + type: 'array', + description: 'Array of search results with memory data, each containing id, data, and score', + }, + ids: { + type: 'array', + description: 'Array of memory IDs found in the search results', + }, + }, request: { url: 'https://api.mem0.ai/v2/memories/search/', method: 'POST', diff --git a/apps/sim/tools/memory/add.ts b/apps/sim/tools/memory/add.ts index 140786c0c8..777179489c 100644 --- a/apps/sim/tools/memory/add.ts +++ b/apps/sim/tools/memory/add.ts @@ -24,6 +24,14 @@ export const memoryAddTool: ToolConfig = { description: 'Content for agent memory', }, }, + outputs: { + success: { type: 'boolean', description: 'Whether the memory was added successfully' }, + memories: { + type: 'array', + description: 'Array of memory objects including the new or updated memory', + }, + error: { type: 'string', description: 'Error message if operation failed' }, + }, request: { url: '/api/memory', method: 'POST', diff --git a/apps/sim/tools/memory/delete.ts b/apps/sim/tools/memory/delete.ts index 449b2afa5d..4c3223c196 100644 --- a/apps/sim/tools/memory/delete.ts +++ b/apps/sim/tools/memory/delete.ts @@ -13,6 +13,11 @@ export const memoryDeleteTool: ToolConfig = { description: 'Identifier for the memory to delete', }, }, + outputs: { + success: { type: 'boolean', description: 'Whether the memory was deleted successfully' }, + message: { type: 'string', description: 'Success or error message' }, + error: { type: 'string', description: 'Error message if operation failed' }, + }, request: { url: (params): any => { // Get workflowId from context (set by workflow execution) diff --git a/apps/sim/tools/memory/get.ts b/apps/sim/tools/memory/get.ts index bc7887ea3c..f07c59ffc9 100644 --- a/apps/sim/tools/memory/get.ts +++ b/apps/sim/tools/memory/get.ts @@ -13,6 +13,12 @@ export const memoryGetTool: ToolConfig = { description: 'Identifier for the memory to retrieve', }, }, + outputs: { + success: { type: 'boolean', description: 'Whether the memory was retrieved successfully' }, + memories: { type: 'array', description: 'Array of memory data for the requested ID' }, + message: { type: 'string', description: 'Success or error message' }, + error: { type: 'string', description: 'Error message if operation failed' }, + }, request: { url: (params): any => { // Get workflowId from context (set by workflow execution) diff --git a/apps/sim/tools/memory/get_all.ts b/apps/sim/tools/memory/get_all.ts index 74243ec93b..230a64d6e1 100644 --- a/apps/sim/tools/memory/get_all.ts +++ b/apps/sim/tools/memory/get_all.ts @@ -7,6 +7,15 @@ export const memoryGetAllTool: ToolConfig = { description: 'Retrieve all memories from the database', version: '1.0.0', params: {}, + outputs: { + success: { type: 'boolean', description: 'Whether all memories were retrieved successfully' }, + memories: { + type: 'array', + description: 'Array of all memory objects with keys, types, and data', + }, + message: { type: 'string', description: 'Success or error message' }, + error: { type: 'string', description: 'Error message if operation failed' }, + }, request: { url: (params): any => { // Get workflowId from context (set by workflow execution) diff --git a/apps/sim/tools/microsoft_excel/read.ts b/apps/sim/tools/microsoft_excel/read.ts index 394499cab2..f0e95cff05 100644 --- a/apps/sim/tools/microsoft_excel/read.ts +++ b/apps/sim/tools/microsoft_excel/read.ts @@ -34,6 +34,31 @@ export const readTool: ToolConfig { const spreadsheetId = params.spreadsheetId?.trim() diff --git a/apps/sim/tools/microsoft_excel/table_add.ts b/apps/sim/tools/microsoft_excel/table_add.ts index 3059a4ecd2..b89cabb2fb 100644 --- a/apps/sim/tools/microsoft_excel/table_add.ts +++ b/apps/sim/tools/microsoft_excel/table_add.ts @@ -43,6 +43,25 @@ export const tableAddTool: ToolConfig< description: 'The data to add to the table (array of arrays or array of objects)', }, }, + outputs: { + success: { type: 'boolean', description: 'Operation success status' }, + output: { + type: 'object', + description: 'Table add operation results and metadata', + properties: { + index: { type: 'number', description: 'Index of the first row that was added' }, + values: { type: 'array', description: 'Array of rows that were added to the table' }, + metadata: { + type: 'object', + description: 'Spreadsheet metadata', + properties: { + spreadsheetId: { type: 'string', description: 'The ID of the spreadsheet' }, + spreadsheetUrl: { type: 'string', description: 'URL to access the spreadsheet' }, + }, + }, + }, + }, + }, request: { url: (params) => { const tableName = encodeURIComponent(params.tableName) diff --git a/apps/sim/tools/microsoft_excel/write.ts b/apps/sim/tools/microsoft_excel/write.ts index abc54cc2ad..2ace7aa921 100644 --- a/apps/sim/tools/microsoft_excel/write.ts +++ b/apps/sim/tools/microsoft_excel/write.ts @@ -52,6 +52,27 @@ export const writeTool: ToolConfig { const rangeInput = params.range?.trim() diff --git a/apps/sim/tools/microsoft_planner/create_task.ts b/apps/sim/tools/microsoft_planner/create_task.ts index 30401ac195..79d4adaede 100644 --- a/apps/sim/tools/microsoft_planner/create_task.ts +++ b/apps/sim/tools/microsoft_planner/create_task.ts @@ -65,6 +65,11 @@ export const createTaskTool: ToolConfig< description: 'The bucket ID to place the task in', }, }, + outputs: { + success: { type: 'boolean', description: 'Whether the task was created successfully' }, + task: { type: 'object', description: 'The created task object with all properties' }, + metadata: { type: 'object', description: 'Metadata including planId, taskId, and taskUrl' }, + }, request: { url: () => 'https://graph.microsoft.com/v1.0/planner/tasks', method: 'POST', diff --git a/apps/sim/tools/microsoft_planner/read_task.ts b/apps/sim/tools/microsoft_planner/read_task.ts index 891f7f7a20..b825284f58 100644 --- a/apps/sim/tools/microsoft_planner/read_task.ts +++ b/apps/sim/tools/microsoft_planner/read_task.ts @@ -38,6 +38,11 @@ export const readTaskTool: ToolConfig { let finalUrl: string diff --git a/apps/sim/tools/microsoft_teams/read_channel.ts b/apps/sim/tools/microsoft_teams/read_channel.ts index a20854a9fb..b183f389b1 100644 --- a/apps/sim/tools/microsoft_teams/read_channel.ts +++ b/apps/sim/tools/microsoft_teams/read_channel.ts @@ -37,6 +37,18 @@ export const readChannelTool: ToolConfig { const teamId = params.teamId?.trim() diff --git a/apps/sim/tools/microsoft_teams/read_chat.ts b/apps/sim/tools/microsoft_teams/read_chat.ts index fa13c83ad6..c8bf0336f2 100644 --- a/apps/sim/tools/microsoft_teams/read_chat.ts +++ b/apps/sim/tools/microsoft_teams/read_chat.ts @@ -28,6 +28,17 @@ export const readChatTool: ToolConfig { // Ensure chatId is valid diff --git a/apps/sim/tools/microsoft_teams/write_channel.ts b/apps/sim/tools/microsoft_teams/write_channel.ts index e4792aa585..59f6f1a330 100644 --- a/apps/sim/tools/microsoft_teams/write_channel.ts +++ b/apps/sim/tools/microsoft_teams/write_channel.ts @@ -39,6 +39,17 @@ export const writeChannelTool: ToolConfig { const teamId = params.teamId?.trim() diff --git a/apps/sim/tools/microsoft_teams/write_chat.ts b/apps/sim/tools/microsoft_teams/write_chat.ts index f71e9be2c0..e7ae1c0418 100644 --- a/apps/sim/tools/microsoft_teams/write_chat.ts +++ b/apps/sim/tools/microsoft_teams/write_chat.ts @@ -33,6 +33,16 @@ export const writeChatTool: ToolConfig { // Ensure chatId is valid diff --git a/apps/sim/tools/mistral/parser.ts b/apps/sim/tools/mistral/parser.ts index b52b978ca7..81172b9014 100644 --- a/apps/sim/tools/mistral/parser.ts +++ b/apps/sim/tools/mistral/parser.ts @@ -63,6 +63,17 @@ export const mistralParserTool: ToolConfig 'https://api.notion.com/v1/databases', diff --git a/apps/sim/tools/notion/create_page.ts b/apps/sim/tools/notion/create_page.ts index 7a8c6aa756..c083eb6e36 100644 --- a/apps/sim/tools/notion/create_page.ts +++ b/apps/sim/tools/notion/create_page.ts @@ -37,6 +37,16 @@ export const notionCreatePageTool: ToolConfig 'https://api.notion.com/v1/pages', diff --git a/apps/sim/tools/notion/query_database.ts b/apps/sim/tools/notion/query_database.ts index 8bb7d8d529..d375d5799b 100644 --- a/apps/sim/tools/notion/query_database.ts +++ b/apps/sim/tools/notion/query_database.ts @@ -43,6 +43,37 @@ export const notionQueryDatabaseTool: ToolConfig { diff --git a/apps/sim/tools/notion/read.ts b/apps/sim/tools/notion/read.ts index 2be63d7ffc..9cfba1599d 100644 --- a/apps/sim/tools/notion/read.ts +++ b/apps/sim/tools/notion/read.ts @@ -25,6 +25,16 @@ export const notionReadTool: ToolConfig = { description: 'The ID of the Notion page to read', }, }, + outputs: { + content: { + type: 'string', + description: 'Page content in markdown format with headers, paragraphs, lists, and todos', + }, + metadata: { + type: 'object', + description: 'Page metadata including title, URL, and timestamps', + }, + }, request: { url: (params: NotionReadParams) => { diff --git a/apps/sim/tools/notion/read_database.ts b/apps/sim/tools/notion/read_database.ts index 9dfc748903..35ce152252 100644 --- a/apps/sim/tools/notion/read_database.ts +++ b/apps/sim/tools/notion/read_database.ts @@ -30,6 +30,16 @@ export const notionReadDatabaseTool: ToolConfig { diff --git a/apps/sim/tools/notion/search.ts b/apps/sim/tools/notion/search.ts index 2903d03d4f..97753b5940 100644 --- a/apps/sim/tools/notion/search.ts +++ b/apps/sim/tools/notion/search.ts @@ -37,6 +37,17 @@ export const notionSearchTool: ToolConfig = description: 'Number of results to return (default: 100, max: 100)', }, }, + outputs: { + content: { + type: 'string', + description: 'Formatted list of search results including pages and databases', + }, + metadata: { + type: 'object', + description: + 'Search metadata including total results count, pagination info, and raw results array', + }, + }, request: { url: () => 'https://api.notion.com/v1/search', diff --git a/apps/sim/tools/notion/update_page.ts b/apps/sim/tools/notion/update_page.ts index 6889173919..03c51e8574 100644 --- a/apps/sim/tools/notion/update_page.ts +++ b/apps/sim/tools/notion/update_page.ts @@ -31,6 +31,16 @@ export const notionUpdatePageTool: ToolConfig { diff --git a/apps/sim/tools/notion/write.ts b/apps/sim/tools/notion/write.ts index 49b55971f4..cdc65c3b0f 100644 --- a/apps/sim/tools/notion/write.ts +++ b/apps/sim/tools/notion/write.ts @@ -31,6 +31,12 @@ export const notionWriteTool: ToolConfig = { description: 'The content to append to the page', }, }, + outputs: { + content: { + type: 'string', + description: 'Success message confirming content was appended to page', + }, + }, request: { url: (params: NotionWriteParams) => { diff --git a/apps/sim/tools/onedrive/create_folder.ts b/apps/sim/tools/onedrive/create_folder.ts index 31d10c5410..68fb05448d 100644 --- a/apps/sim/tools/onedrive/create_folder.ts +++ b/apps/sim/tools/onedrive/create_folder.ts @@ -37,6 +37,14 @@ export const createFolderTool: ToolConfig { // Use specific parent folder URL if parentId is provided diff --git a/apps/sim/tools/onedrive/list.ts b/apps/sim/tools/onedrive/list.ts index f8c8315b51..0ca4239b8a 100644 --- a/apps/sim/tools/onedrive/list.ts +++ b/apps/sim/tools/onedrive/list.ts @@ -54,6 +54,14 @@ export const listTool: ToolConfig = { description: 'The number of files to return', }, }, + outputs: { + success: { type: 'boolean', description: 'Whether files were listed successfully' }, + files: { type: 'array', description: 'Array of file and folder objects with metadata' }, + nextPageToken: { + type: 'string', + description: 'Token for retrieving the next page of results (optional)', + }, + }, request: { url: (params) => { // Use specific folder if provided, otherwise use root diff --git a/apps/sim/tools/onedrive/upload.ts b/apps/sim/tools/onedrive/upload.ts index 5783351bb5..7a70d76d0d 100644 --- a/apps/sim/tools/onedrive/upload.ts +++ b/apps/sim/tools/onedrive/upload.ts @@ -53,6 +53,14 @@ export const uploadTool: ToolConfig description: 'The ID of the folder to upload the file to (internal use)', }, }, + outputs: { + success: { type: 'boolean', description: 'Whether the file was uploaded successfully' }, + file: { + type: 'object', + description: + 'The uploaded file object with metadata including id, name, webViewLink, webContentLink, and timestamps', + }, + }, request: { url: (params) => { let fileName = params.fileName || 'untitled' diff --git a/apps/sim/tools/openai/embeddings.ts b/apps/sim/tools/openai/embeddings.ts index 69e854d62d..17b427b5f1 100644 --- a/apps/sim/tools/openai/embeddings.ts +++ b/apps/sim/tools/openai/embeddings.ts @@ -35,7 +35,25 @@ export const embeddingsTool: ToolConfig = { description: 'OpenAI API key', }, }, - + outputs: { + success: { type: 'boolean', description: 'Operation success status' }, + output: { + type: 'object', + description: 'Embeddings generation results', + properties: { + embeddings: { type: 'array', description: 'Array of embedding vectors' }, + model: { type: 'string', description: 'Model used for generating embeddings' }, + usage: { + type: 'object', + description: 'Token usage information', + properties: { + prompt_tokens: { type: 'number', description: 'Number of tokens in the prompt' }, + total_tokens: { type: 'number', description: 'Total number of tokens used' }, + }, + }, + }, + }, + }, request: { method: 'POST', url: () => 'https://api.openai.com/v1/embeddings', diff --git a/apps/sim/tools/openai/image.ts b/apps/sim/tools/openai/image.ts index 85d0e2eef4..75d7c33197 100644 --- a/apps/sim/tools/openai/image.ts +++ b/apps/sim/tools/openai/image.ts @@ -60,6 +60,24 @@ export const imageTool: ToolConfig = { description: 'Your OpenAI API key', }, }, + outputs: { + success: { type: 'boolean', description: 'Operation success status' }, + output: { + type: 'object', + description: 'Generated image data', + properties: { + content: { type: 'string', description: 'Image URL or identifier' }, + image: { type: 'string', description: 'Base64 encoded image data' }, + metadata: { + type: 'object', + description: 'Image generation metadata', + properties: { + model: { type: 'string', description: 'Model used for image generation' }, + }, + }, + }, + }, + }, request: { url: 'https://api.openai.com/v1/images/generations', method: 'POST', diff --git a/apps/sim/tools/outlook/draft.ts b/apps/sim/tools/outlook/draft.ts index e406cc5e2c..9166e57283 100644 --- a/apps/sim/tools/outlook/draft.ts +++ b/apps/sim/tools/outlook/draft.ts @@ -37,6 +37,27 @@ export const outlookDraftTool: ToolConfig => { - return { + // Helper function to parse comma-separated emails + const parseEmails = (emailString?: string) => { + if (!emailString) return [] + return emailString + .split(',') + .map((email) => email.trim()) + .filter((email) => email.length > 0) + .map((email) => ({ emailAddress: { address: email } })) + } + + const message: any = { subject: params.subject, body: { contentType: 'Text', content: params.body, }, - toRecipients: [ - { - emailAddress: { - address: params.to, - }, - }, - ], + toRecipients: parseEmails(params.to), + } + + // Add CC if provided + const ccRecipients = parseEmails(params.cc) + if (ccRecipients.length > 0) { + message.ccRecipients = ccRecipients + } + + // Add BCC if provided + const bccRecipients = parseEmails(params.bcc) + if (bccRecipients.length > 0) { + message.bccRecipients = bccRecipients } + + return message }, }, transformResponse: async (response) => { diff --git a/apps/sim/tools/outlook/read.ts b/apps/sim/tools/outlook/read.ts index 14ca192f8e..298fe1b50b 100644 --- a/apps/sim/tools/outlook/read.ts +++ b/apps/sim/tools/outlook/read.ts @@ -37,6 +37,13 @@ export const outlookReadTool: ToolConfig description: 'Maximum number of emails to retrieve (default: 1, max: 10)', }, }, + + outputs: { + success: { type: 'boolean', description: 'Email read operation success status' }, + messageCount: { type: 'number', description: 'Number of emails retrieved' }, + messages: { type: 'array', description: 'Array of email message objects' }, + message: { type: 'string', description: 'Success or status message' }, + }, request: { url: (params) => { // Set max results (default to 1 for simplicity, max 10) with no negative values diff --git a/apps/sim/tools/outlook/send.ts b/apps/sim/tools/outlook/send.ts index 1b332e6ec8..24347ab074 100644 --- a/apps/sim/tools/outlook/send.ts +++ b/apps/sim/tools/outlook/send.ts @@ -63,6 +63,13 @@ export const outlookSendTool: ToolConfig }, }, + outputs: { + success: { type: 'boolean', description: 'Email send success status' }, + status: { type: 'string', description: 'Delivery status of the email' }, + timestamp: { type: 'string', description: 'Timestamp when email was sent' }, + message: { type: 'string', description: 'Success or error message' }, + }, + request: { url: (params) => { // If replying to a specific message, use the reply endpoint diff --git a/apps/sim/tools/outlook/types.ts b/apps/sim/tools/outlook/types.ts index 02024398d0..be0ce5456e 100644 --- a/apps/sim/tools/outlook/types.ts +++ b/apps/sim/tools/outlook/types.ts @@ -36,6 +36,8 @@ export interface OutlookReadResponse extends ToolResponse { export interface OutlookDraftParams { accessToken: string to: string + cc?: string + bcc?: string subject: string body: string } diff --git a/apps/sim/tools/perplexity/chat.ts b/apps/sim/tools/perplexity/chat.ts index a984d9adfb..ab8cbca29b 100644 --- a/apps/sim/tools/perplexity/chat.ts +++ b/apps/sim/tools/perplexity/chat.ts @@ -45,7 +45,29 @@ export const chatTool: ToolConfig description: 'Perplexity API key', }, }, - + outputs: { + success: { type: 'boolean', description: 'Operation success status' }, + output: { + type: 'object', + description: 'Chat completion results', + properties: { + content: { type: 'string', description: 'Generated text content' }, + model: { type: 'string', description: 'Model used for generation' }, + usage: { + type: 'object', + description: 'Token usage information', + properties: { + prompt_tokens: { type: 'number', description: 'Number of tokens in the prompt' }, + completion_tokens: { + type: 'number', + description: 'Number of tokens in the completion', + }, + total_tokens: { type: 'number', description: 'Total number of tokens used' }, + }, + }, + }, + }, + }, request: { method: 'POST', url: () => 'https://api.perplexity.ai/chat/completions', diff --git a/apps/sim/tools/pinecone/fetch.ts b/apps/sim/tools/pinecone/fetch.ts index 76975561d3..f183bd4f98 100644 --- a/apps/sim/tools/pinecone/fetch.ts +++ b/apps/sim/tools/pinecone/fetch.ts @@ -34,6 +34,40 @@ export const fetchTool: ToolConfig = { }, }, + outputs: { + matches: { + type: 'array', + description: 'Fetched vectors with ID, values, metadata, and score', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Vector ID' }, + values: { type: 'array', description: 'Vector values' }, + metadata: { type: 'object', description: 'Associated metadata' }, + score: { type: 'number', description: 'Match score (1.0 for exact matches)' }, + }, + }, + }, + data: { + type: 'array', + description: 'Vector data with values and vector type', + items: { + type: 'object', + properties: { + values: { type: 'array', description: 'Vector values' }, + vector_type: { type: 'string', description: 'Vector type (dense/sparse)' }, + }, + }, + }, + usage: { + type: 'object', + description: 'Usage statistics including total read units', + properties: { + total_tokens: { type: 'number', description: 'Read units consumed' }, + }, + }, + }, + request: { method: 'GET', url: (params) => { diff --git a/apps/sim/tools/pinecone/generate_embeddings.ts b/apps/sim/tools/pinecone/generate_embeddings.ts index 7490cae774..356f1336fe 100644 --- a/apps/sim/tools/pinecone/generate_embeddings.ts +++ b/apps/sim/tools/pinecone/generate_embeddings.ts @@ -31,6 +31,25 @@ export const generateEmbeddingsTool: ToolConfig< }, }, + outputs: { + data: { + type: 'array', + description: 'Generated embeddings data with values and vector type', + }, + model: { + type: 'string', + description: 'Model used for generating embeddings', + }, + vector_type: { + type: 'string', + description: 'Type of vector generated (dense/sparse)', + }, + usage: { + type: 'object', + description: 'Usage statistics for embeddings generation', + }, + }, + request: { method: 'POST', url: () => 'https://api.pinecone.io/embed', diff --git a/apps/sim/tools/pinecone/search_text.ts b/apps/sim/tools/pinecone/search_text.ts index d910896a33..966db468be 100644 --- a/apps/sim/tools/pinecone/search_text.ts +++ b/apps/sim/tools/pinecone/search_text.ts @@ -62,6 +62,30 @@ export const searchTextTool: ToolConfig `${params.indexHost}/records/namespaces/${params.namespace}/search`, diff --git a/apps/sim/tools/pinecone/search_vector.ts b/apps/sim/tools/pinecone/search_vector.ts index c750af99de..4754a773d5 100644 --- a/apps/sim/tools/pinecone/search_vector.ts +++ b/apps/sim/tools/pinecone/search_vector.ts @@ -58,6 +58,17 @@ export const searchVectorTool: ToolConfig `${params.indexHost}/query`, diff --git a/apps/sim/tools/pinecone/upsert_text.ts b/apps/sim/tools/pinecone/upsert_text.ts index ebd28fa4ab..851a4ec551 100644 --- a/apps/sim/tools/pinecone/upsert_text.ts +++ b/apps/sim/tools/pinecone/upsert_text.ts @@ -39,6 +39,17 @@ export const upsertTextTool: ToolConfig `${params.indexHost}/records/namespaces/${params.namespace}/upsert`, diff --git a/apps/sim/tools/qdrant/fetch_points.ts b/apps/sim/tools/qdrant/fetch_points.ts index b4a1640d49..d44a36d8e9 100644 --- a/apps/sim/tools/qdrant/fetch_points.ts +++ b/apps/sim/tools/qdrant/fetch_points.ts @@ -46,6 +46,17 @@ export const fetchPointsTool: ToolConfig = { }, }, + outputs: { + data: { + type: 'array', + description: 'Fetched points with ID, payload, and optional vector data', + }, + status: { + type: 'string', + description: 'Status of the fetch operation', + }, + }, + request: { method: 'POST', url: (params) => `${params.url.replace(/\/$/, '')}/collections/${params.collection}/points`, diff --git a/apps/sim/tools/qdrant/search_vector.ts b/apps/sim/tools/qdrant/search_vector.ts index 92a8eac8ed..2fd6f6e887 100644 --- a/apps/sim/tools/qdrant/search_vector.ts +++ b/apps/sim/tools/qdrant/search_vector.ts @@ -58,6 +58,17 @@ export const searchVectorTool: ToolConfig = }, }, + outputs: { + data: { + type: 'array', + description: 'Vector search results with ID, score, payload, and optional vector data', + }, + status: { + type: 'string', + description: 'Status of the search operation', + }, + }, + request: { method: 'POST', url: (params) => diff --git a/apps/sim/tools/qdrant/upsert_points.ts b/apps/sim/tools/qdrant/upsert_points.ts index 2550707bb5..143f978c08 100644 --- a/apps/sim/tools/qdrant/upsert_points.ts +++ b/apps/sim/tools/qdrant/upsert_points.ts @@ -33,6 +33,17 @@ export const upsertPointsTool: ToolConfig = }, }, + outputs: { + status: { + type: 'string', + description: 'Status of the upsert operation', + }, + data: { + type: 'object', + description: 'Result data from the upsert operation', + }, + }, + request: { method: 'PUT', url: (params) => `${params.url.replace(/\/$/, '')}/collections/${params.collection}/points`, diff --git a/apps/sim/tools/reddit/get_comments.ts b/apps/sim/tools/reddit/get_comments.ts index 841e7105df..43fceeb332 100644 --- a/apps/sim/tools/reddit/get_comments.ts +++ b/apps/sim/tools/reddit/get_comments.ts @@ -46,6 +46,42 @@ export const getCommentsTool: ToolConfig { // Sanitize inputs diff --git a/apps/sim/tools/reddit/get_posts.ts b/apps/sim/tools/reddit/get_posts.ts index 38a9118a21..de1eec9310 100644 --- a/apps/sim/tools/reddit/get_posts.ts +++ b/apps/sim/tools/reddit/get_posts.ts @@ -47,6 +47,34 @@ export const getPostsTool: ToolConfig = }, }, + outputs: { + subreddit: { + type: 'string', + description: 'Name of the subreddit where posts were fetched from', + }, + posts: { + type: 'array', + description: 'Array of posts with title, author, URL, score, comments count, and metadata', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Post ID' }, + title: { type: 'string', description: 'Post title' }, + author: { type: 'string', description: 'Author username' }, + url: { type: 'string', description: 'Post URL' }, + permalink: { type: 'string', description: 'Reddit permalink' }, + score: { type: 'number', description: 'Post score (upvotes - downvotes)' }, + num_comments: { type: 'number', description: 'Number of comments' }, + created_utc: { type: 'number', description: 'Creation timestamp (UTC)' }, + is_self: { type: 'boolean', description: 'Whether this is a text post' }, + selftext: { type: 'string', description: 'Text content for self posts' }, + thumbnail: { type: 'string', description: 'Thumbnail URL' }, + subreddit: { type: 'string', description: 'Subreddit name' }, + }, + }, + }, + }, + request: { url: (params: RedditPostsParams) => { // Sanitize inputs diff --git a/apps/sim/tools/reddit/hot_posts.ts b/apps/sim/tools/reddit/hot_posts.ts index daf7adf694..4909ba95bb 100644 --- a/apps/sim/tools/reddit/hot_posts.ts +++ b/apps/sim/tools/reddit/hot_posts.ts @@ -40,6 +40,18 @@ export const hotPostsTool: ToolConfig = }, }, + outputs: { + subreddit: { + type: 'string', + description: 'Name of the subreddit where hot posts were fetched from', + }, + posts: { + type: 'array', + description: + 'Array of hot posts with title, author, URL, score, comments count, and metadata', + }, + }, + request: { url: (params) => { // Sanitize inputs and enforce limits diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 84f33c7ecd..4be4524938 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -1,3 +1,4 @@ +// Provider tools - handled separately import { airtableCreateRecordsTool, airtableGetRecordTool, @@ -311,4 +312,6 @@ export const tools: Record = { sharepoint_create_page: sharepointCreatePageTool, sharepoint_read_page: sharepointReadPageTool, sharepoint_list_sites: sharepointListSitesTool, + // Provider chat tools + // Provider chat tools - handled separately in agent blocks } diff --git a/apps/sim/tools/s3/get_object.ts b/apps/sim/tools/s3/get_object.ts index a88f676f96..2e2af6a4fd 100644 --- a/apps/sim/tools/s3/get_object.ts +++ b/apps/sim/tools/s3/get_object.ts @@ -32,6 +32,17 @@ export const s3GetObjectTool: ToolConfig = { description: 'S3 Object URL', }, }, + + outputs: { + url: { + type: 'string', + description: 'Pre-signed URL for downloading the S3 object', + }, + metadata: { + type: 'object', + description: 'File metadata including type, size, name, and last modified date', + }, + }, request: { url: (params) => { try { diff --git a/apps/sim/tools/serper/search.ts b/apps/sim/tools/serper/search.ts index e9ec6c56b4..53f263cab9 100644 --- a/apps/sim/tools/serper/search.ts +++ b/apps/sim/tools/serper/search.ts @@ -47,6 +47,14 @@ export const searchTool: ToolConfig = { }, }, + outputs: { + searchResults: { + type: 'array', + description: + 'Search results with titles, links, snippets, and type-specific metadata (date for news, rating for places, imageUrl for images)', + }, + }, + request: { url: (params) => `https://google.serper.dev/${params.type || 'search'}`, method: 'POST', diff --git a/apps/sim/tools/sharepoint/create_page.ts b/apps/sim/tools/sharepoint/create_page.ts index 235ecac6cd..5817e1b396 100644 --- a/apps/sim/tools/sharepoint/create_page.ts +++ b/apps/sim/tools/sharepoint/create_page.ts @@ -56,6 +56,21 @@ export const createPageTool: ToolConfig { // Use specific site if provided, otherwise use root site diff --git a/apps/sim/tools/sharepoint/list_sites.ts b/apps/sim/tools/sharepoint/list_sites.ts index d7876a47be..42189eac1b 100644 --- a/apps/sim/tools/sharepoint/list_sites.ts +++ b/apps/sim/tools/sharepoint/list_sites.ts @@ -35,6 +35,50 @@ export const listSitesTool: ToolConfig { let baseUrl: string diff --git a/apps/sim/tools/sharepoint/read_page.ts b/apps/sim/tools/sharepoint/read_page.ts index 36a0685993..e4a574ac6b 100644 --- a/apps/sim/tools/sharepoint/read_page.ts +++ b/apps/sim/tools/sharepoint/read_page.ts @@ -59,6 +59,64 @@ export const readPageTool: ToolConfig { // Use specific site if provided, otherwise use root site diff --git a/apps/sim/tools/slack/canvas.ts b/apps/sim/tools/slack/canvas.ts index 1f1a40b7d0..9f161d2465 100644 --- a/apps/sim/tools/slack/canvas.ts +++ b/apps/sim/tools/slack/canvas.ts @@ -67,6 +67,12 @@ export const slackCanvasTool: ToolConfig }, }, + outputs: { + canvas_id: { type: 'string', description: 'ID of the created canvas' }, + channel: { type: 'string', description: 'Channel where canvas was created' }, + title: { type: 'string', description: 'Title of the canvas' }, + }, + request: { url: 'https://slack.com/api/canvases.create', method: 'POST', diff --git a/apps/sim/tools/slack/message.ts b/apps/sim/tools/slack/message.ts index ab5035de74..ec82c75568 100644 --- a/apps/sim/tools/slack/message.ts +++ b/apps/sim/tools/slack/message.ts @@ -53,6 +53,11 @@ export const slackMessageTool: ToolConfig { const url = new URL('https://slack.com/api/conversations.history') diff --git a/apps/sim/tools/stagehand/agent.ts b/apps/sim/tools/stagehand/agent.ts index 604586ff86..36744a8f5d 100644 --- a/apps/sim/tools/stagehand/agent.ts +++ b/apps/sim/tools/stagehand/agent.ts @@ -43,6 +43,33 @@ export const agentTool: ToolConfig description: 'Optional JSON schema defining the structure of data the agent should return', }, }, + outputs: { + agentResult: { + type: 'object', + description: 'Result from the Stagehand agent execution', + properties: { + success: { type: 'boolean', description: 'Whether the agent task completed successfully' }, + completed: { type: 'boolean', description: 'Whether the task was fully completed' }, + message: { type: 'string', description: 'Status message or final result' }, + actions: { + type: 'array', + items: { + type: 'object', + properties: { + type: { type: 'string', description: 'Type of action performed' }, + params: { type: 'object', description: 'Parameters used for the action' }, + result: { type: 'object', description: 'Result of the action' }, + }, + }, + description: 'List of actions performed by the agent', + }, + }, + }, + structuredOutput: { + type: 'object', + description: 'Extracted data matching the provided output schema', + }, + }, request: { url: '/api/tools/stagehand/agent', diff --git a/apps/sim/tools/stagehand/extract.ts b/apps/sim/tools/stagehand/extract.ts index 56d58b65e5..93f2f83e1a 100644 --- a/apps/sim/tools/stagehand/extract.ts +++ b/apps/sim/tools/stagehand/extract.ts @@ -36,6 +36,12 @@ export const extractTool: ToolConfig `https://${params.projectId}.supabase.co/rest/v1/${params.table}?select=*`, method: 'DELETE', diff --git a/apps/sim/tools/supabase/get_row.ts b/apps/sim/tools/supabase/get_row.ts index bd492d83e0..2b47cb879c 100644 --- a/apps/sim/tools/supabase/get_row.ts +++ b/apps/sim/tools/supabase/get_row.ts @@ -32,6 +32,18 @@ export const getRowTool: ToolConfig `https://${params.projectId}.supabase.co/rest/v1/${params.table}`, method: 'GET', diff --git a/apps/sim/tools/supabase/insert.ts b/apps/sim/tools/supabase/insert.ts index a1ddb9a321..705772731d 100644 --- a/apps/sim/tools/supabase/insert.ts +++ b/apps/sim/tools/supabase/insert.ts @@ -32,6 +32,18 @@ export const insertTool: ToolConfig `https://${params.projectId}.supabase.co/rest/v1/${params.table}?select=*`, method: 'POST', diff --git a/apps/sim/tools/supabase/query.ts b/apps/sim/tools/supabase/query.ts index d37de91260..f78870aa5c 100644 --- a/apps/sim/tools/supabase/query.ts +++ b/apps/sim/tools/supabase/query.ts @@ -44,6 +44,18 @@ export const queryTool: ToolConfig = description: 'Your Supabase service role secret key', }, }, + outputs: { + success: { type: 'boolean', description: 'Operation success status' }, + output: { + type: 'object', + description: 'Query operation results', + properties: { + message: { type: 'string', description: 'Operation status message' }, + results: { type: 'array', description: 'Array of records returned from the query' }, + }, + }, + error: { type: 'string', description: 'Error message if the operation failed' }, + }, request: { url: (params) => `https://${params.projectId}.supabase.co/rest/v1/${params.table}`, method: 'GET', diff --git a/apps/sim/tools/supabase/update.ts b/apps/sim/tools/supabase/update.ts index d8046de756..1bcf77e1b8 100644 --- a/apps/sim/tools/supabase/update.ts +++ b/apps/sim/tools/supabase/update.ts @@ -38,6 +38,18 @@ export const updateTool: ToolConfig `https://${params.projectId}.supabase.co/rest/v1/${params.table}?select=*`, method: 'PATCH', diff --git a/apps/sim/tools/tavily/extract.ts b/apps/sim/tools/tavily/extract.ts index 3403def969..29355c65da 100644 --- a/apps/sim/tools/tavily/extract.ts +++ b/apps/sim/tools/tavily/extract.ts @@ -28,6 +28,34 @@ export const extractTool: ToolConfig description: 'Tavily API Key', }, }, + outputs: { + results: { + type: 'array', + items: { + type: 'object', + properties: { + url: { type: 'string', description: 'The URL that was extracted' }, + raw_content: { type: 'string', description: 'The raw text content from the webpage' }, + }, + }, + description: 'Successfully extracted content from URLs', + }, + failed_results: { + type: 'array', + items: { + type: 'object', + properties: { + url: { type: 'string', description: 'The URL that failed extraction' }, + error: { type: 'string', description: 'Error message for the failed extraction' }, + }, + }, + description: 'URLs that failed to extract content', + }, + response_time: { + type: 'number', + description: 'Time taken for the extraction request in seconds', + }, + }, request: { url: 'https://api.tavily.com/extract', diff --git a/apps/sim/tools/tavily/search.ts b/apps/sim/tools/tavily/search.ts index c900973d8f..adce0d9ec7 100644 --- a/apps/sim/tools/tavily/search.ts +++ b/apps/sim/tools/tavily/search.ts @@ -28,6 +28,23 @@ export const searchTool: ToolConfig = description: 'Tavily API Key', }, }, + outputs: { + query: { type: 'string', description: 'The search query that was executed' }, + results: { + type: 'array', + items: { + type: 'object', + properties: { + title: { type: 'string' }, + url: { type: 'string' }, + snippet: { type: 'string' }, + raw_content: { type: 'string' }, + }, + }, + description: 'Search results with titles, URLs, and content snippets', + }, + response_time: { type: 'number', description: 'Time taken for the search request in seconds' }, + }, request: { url: 'https://api.tavily.com/search', diff --git a/apps/sim/tools/telegram/message.ts b/apps/sim/tools/telegram/message.ts index 0235365cc6..dfb1b03688 100644 --- a/apps/sim/tools/telegram/message.ts +++ b/apps/sim/tools/telegram/message.ts @@ -30,6 +30,15 @@ export const telegramMessageTool: ToolConfig `https://api.telegram.org/bot${params.botToken}/sendMessage`, diff --git a/apps/sim/tools/thinking/tool.ts b/apps/sim/tools/thinking/tool.ts index 939648e608..0b7907fddb 100644 --- a/apps/sim/tools/thinking/tool.ts +++ b/apps/sim/tools/thinking/tool.ts @@ -18,6 +18,13 @@ export const thinkingTool: ToolConfig }, }, + outputs: { + acknowledgedThought: { + type: 'string', + description: 'The thought that was processed and acknowledged', + }, + }, + // Use directExecution as no external HTTP call is needed directExecution: async (params: ThinkingToolParams): Promise => { // Simply acknowledge the thought by returning it in the output diff --git a/apps/sim/tools/twilio/send_sms.ts b/apps/sim/tools/twilio/send_sms.ts index c6bf31f0e5..eaeeefd492 100644 --- a/apps/sim/tools/twilio/send_sms.ts +++ b/apps/sim/tools/twilio/send_sms.ts @@ -43,6 +43,14 @@ export const sendSMSTool: ToolConfig }, }, + outputs: { + success: { type: 'boolean', description: 'SMS send success status' }, + messageId: { type: 'string', description: 'Unique Twilio message identifier (SID)' }, + status: { type: 'string', description: 'Message delivery status from Twilio' }, + fromNumber: { type: 'string', description: 'Phone number message was sent from' }, + toNumber: { type: 'string', description: 'Phone number message was sent to' }, + }, + request: { url: (params) => { if (!params.accountSid) { diff --git a/apps/sim/tools/typeform/files.ts b/apps/sim/tools/typeform/files.ts index dd0ea89b8e..4ab57d8b42 100644 --- a/apps/sim/tools/typeform/files.ts +++ b/apps/sim/tools/typeform/files.ts @@ -44,6 +44,11 @@ export const filesTool: ToolConfig = description: 'Typeform Personal Access Token', }, }, + outputs: { + fileUrl: { type: 'string', description: 'Direct download URL for the uploaded file' }, + contentType: { type: 'string', description: 'MIME type of the uploaded file' }, + filename: { type: 'string', description: 'Original filename of the uploaded file' }, + }, request: { url: (params: TypeformFilesParams) => { const encodedFormId = encodeURIComponent(params.formId) diff --git a/apps/sim/tools/typeform/insights.ts b/apps/sim/tools/typeform/insights.ts index b56a02cf39..4060933e6d 100644 --- a/apps/sim/tools/typeform/insights.ts +++ b/apps/sim/tools/typeform/insights.ts @@ -23,6 +23,65 @@ export const insightsTool: ToolConfig { const encodedFormId = encodeURIComponent(params.formId) diff --git a/apps/sim/tools/types.ts b/apps/sim/tools/types.ts index 83777f5b5f..792654c8e9 100644 --- a/apps/sim/tools/types.ts +++ b/apps/sim/tools/types.ts @@ -2,6 +2,18 @@ import type { OAuthService } from '@/lib/oauth/oauth' export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' +export interface OutputProperty { + type: string + description?: string + optional?: boolean + properties?: Record + items?: { + type: string + description?: string + properties?: Record + } +} + export type ParameterVisibility = | 'user-or-llm' // User can provide OR LLM must generate | 'user-only' // Only user can provide (required/optional determined by required field) @@ -44,6 +56,25 @@ export interface ToolConfig

{ } > + // Output schema - what this tool produces + outputs?: Record< + string, + { + type: 'string' | 'number' | 'boolean' | 'json' | 'file' | 'file[]' | 'array' | 'object' + description?: string + optional?: boolean + fileConfig?: { + mimeType?: string // Expected MIME type for file outputs + extension?: string // Expected file extension + } + items?: { + type: string + properties?: Record + } + properties?: Record + } + > + // OAuth configuration for this tool (if it requires authentication) oauth?: OAuthConfig @@ -83,3 +114,14 @@ export interface OAuthTokenPayload { credentialId: string workflowId?: string } + +/** + * File data that tools can return for file-typed outputs + */ +export interface ToolFileData { + name: string + mimeType: string + data?: Buffer | string // Buffer or base64 string + url?: string // URL to download file from + size?: number +} diff --git a/apps/sim/tools/vision/tool.ts b/apps/sim/tools/vision/tool.ts index 6bf3e43b7a..876699f270 100644 --- a/apps/sim/tools/vision/tool.ts +++ b/apps/sim/tools/vision/tool.ts @@ -35,6 +35,33 @@ export const visionTool: ToolConfig = { }, }, + outputs: { + content: { + type: 'string', + description: 'The analyzed content and description of the image', + }, + model: { + type: 'string', + description: 'The vision model that was used for analysis', + optional: true, + }, + tokens: { + type: 'number', + description: 'Total tokens used for the analysis', + optional: true, + }, + usage: { + type: 'object', + description: 'Detailed token usage breakdown', + optional: true, + properties: { + input_tokens: { type: 'number', description: 'Tokens used for input processing' }, + output_tokens: { type: 'number', description: 'Tokens used for response generation' }, + total_tokens: { type: 'number', description: 'Total tokens consumed' }, + }, + }, + }, + request: { method: 'POST', url: (params) => { @@ -122,6 +149,14 @@ export const visionTool: ToolConfig = { tokens: data.content ? data.usage?.input_tokens + data.usage?.output_tokens : data.usage?.total_tokens, + usage: data.usage + ? { + input_tokens: data.usage.input_tokens, + output_tokens: data.usage.output_tokens, + total_tokens: + data.usage.total_tokens || data.usage.input_tokens + data.usage.output_tokens, + } + : undefined, }, } }, diff --git a/apps/sim/tools/wealthbox/read_contact.ts b/apps/sim/tools/wealthbox/read_contact.ts index 8c3ead67d8..cc7dc65503 100644 --- a/apps/sim/tools/wealthbox/read_contact.ts +++ b/apps/sim/tools/wealthbox/read_contact.ts @@ -23,6 +23,26 @@ export const wealthboxReadContactTool: ToolConfig { const contactId = params.contactId?.trim() diff --git a/apps/sim/tools/wealthbox/read_note.ts b/apps/sim/tools/wealthbox/read_note.ts index a51a9044e0..f307220f90 100644 --- a/apps/sim/tools/wealthbox/read_note.ts +++ b/apps/sim/tools/wealthbox/read_note.ts @@ -23,6 +23,26 @@ export const wealthboxReadNoteTool: ToolConfig { const noteId = params.noteId?.trim() diff --git a/apps/sim/tools/wealthbox/read_task.ts b/apps/sim/tools/wealthbox/read_task.ts index 1b850910f1..3c8fbf6d7a 100644 --- a/apps/sim/tools/wealthbox/read_task.ts +++ b/apps/sim/tools/wealthbox/read_task.ts @@ -23,6 +23,26 @@ export const wealthboxReadTaskTool: ToolConfig { const taskId = params.taskId?.trim() diff --git a/apps/sim/tools/wealthbox/write_contact.ts b/apps/sim/tools/wealthbox/write_contact.ts index 9f1d14e8f3..0b153a19f8 100644 --- a/apps/sim/tools/wealthbox/write_contact.ts +++ b/apps/sim/tools/wealthbox/write_contact.ts @@ -92,6 +92,26 @@ export const wealthboxWriteContactTool: ToolConfig { const taskId = params.taskId?.trim() diff --git a/apps/sim/tools/whatsapp/send_message.ts b/apps/sim/tools/whatsapp/send_message.ts index c5015f0967..f43d3f23df 100644 --- a/apps/sim/tools/whatsapp/send_message.ts +++ b/apps/sim/tools/whatsapp/send_message.ts @@ -37,6 +37,14 @@ export const sendMessageTool: ToolConfig { if (!params.phoneNumberId) { diff --git a/apps/sim/tools/wikipedia/content.ts b/apps/sim/tools/wikipedia/content.ts index 91d4e34d59..8cbf74d116 100644 --- a/apps/sim/tools/wikipedia/content.ts +++ b/apps/sim/tools/wikipedia/content.ts @@ -20,6 +20,20 @@ export const pageContentTool: ToolConfig { const encodedTitle = encodeURIComponent(params.pageTitle.replace(/ /g, '_')) diff --git a/apps/sim/tools/wikipedia/random.ts b/apps/sim/tools/wikipedia/random.ts index 29978f6469..49a7da9653 100644 --- a/apps/sim/tools/wikipedia/random.ts +++ b/apps/sim/tools/wikipedia/random.ts @@ -9,6 +9,20 @@ export const randomPageTool: ToolConfig, WikipediaRandomPa params: {}, + outputs: { + randomPage: { + type: 'object', + description: 'Random Wikipedia page data', + properties: { + title: { type: 'string', description: 'Page title' }, + extract: { type: 'string', description: 'Page extract/summary' }, + description: { type: 'string', description: 'Page description', optional: true }, + thumbnail: { type: 'object', description: 'Thumbnail image data', optional: true }, + content_urls: { type: 'object', description: 'URLs to access the page' }, + }, + }, + }, + request: { url: () => { return 'https://en.wikipedia.org/api/rest_v1/page/random/summary' diff --git a/apps/sim/tools/wikipedia/search.ts b/apps/sim/tools/wikipedia/search.ts index ad95f1b0fb..bda3dcb3ee 100644 --- a/apps/sim/tools/wikipedia/search.ts +++ b/apps/sim/tools/wikipedia/search.ts @@ -22,6 +22,29 @@ export const searchTool: ToolConfig { const baseUrl = 'https://en.wikipedia.org/w/api.php' diff --git a/apps/sim/tools/wikipedia/summary.ts b/apps/sim/tools/wikipedia/summary.ts index 2e20783a99..886e548f14 100644 --- a/apps/sim/tools/wikipedia/summary.ts +++ b/apps/sim/tools/wikipedia/summary.ts @@ -20,6 +20,21 @@ export const pageSummaryTool: ToolConfig { const encodedTitle = encodeURIComponent(params.pageTitle.replace(/ /g, '_')) diff --git a/apps/sim/tools/x/read.ts b/apps/sim/tools/x/read.ts index 66a30a883e..d40df1850b 100644 --- a/apps/sim/tools/x/read.ts +++ b/apps/sim/tools/x/read.ts @@ -34,6 +34,24 @@ export const xReadTool: ToolConfig = { }, }, + outputs: { + tweet: { + type: 'object', + description: 'The main tweet data', + properties: { + id: { type: 'string', description: 'Tweet ID' }, + text: { type: 'string', description: 'Tweet content text' }, + createdAt: { type: 'string', description: 'Tweet creation timestamp' }, + authorId: { type: 'string', description: 'ID of the tweet author' }, + }, + }, + context: { + type: 'object', + description: 'Conversation context including parent and root tweets', + optional: true, + }, + }, + request: { url: (params) => { const expansions = [ diff --git a/apps/sim/tools/x/search.ts b/apps/sim/tools/x/search.ts index 94f0afbfab..7266006e71 100644 --- a/apps/sim/tools/x/search.ts +++ b/apps/sim/tools/x/search.ts @@ -52,6 +52,36 @@ export const xSearchTool: ToolConfig = { }, }, + outputs: { + tweets: { + type: 'array', + description: 'Array of tweets matching the search query', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Tweet ID' }, + text: { type: 'string', description: 'Tweet content' }, + createdAt: { type: 'string', description: 'Creation timestamp' }, + authorId: { type: 'string', description: 'Author user ID' }, + }, + }, + }, + includes: { + type: 'object', + description: 'Additional data including user profiles and media', + optional: true, + }, + meta: { + type: 'object', + description: 'Search metadata including result count and pagination tokens', + properties: { + resultCount: { type: 'number', description: 'Number of results returned' }, + newestId: { type: 'string', description: 'ID of the newest tweet' }, + oldestId: { type: 'string', description: 'ID of the oldest tweet' }, + }, + }, + }, + request: { url: (params) => { const query = params.query diff --git a/apps/sim/tools/x/user.ts b/apps/sim/tools/x/user.ts index 5a19702233..0bcb212b71 100644 --- a/apps/sim/tools/x/user.ts +++ b/apps/sim/tools/x/user.ts @@ -31,6 +31,29 @@ export const xUserTool: ToolConfig = { }, }, + outputs: { + user: { + type: 'object', + description: 'X user profile information', + properties: { + id: { type: 'string', description: 'User ID' }, + username: { type: 'string', description: 'Username without @ symbol' }, + name: { type: 'string', description: 'Display name' }, + description: { type: 'string', description: 'User bio/description', optional: true }, + verified: { type: 'boolean', description: 'Whether the user is verified' }, + metrics: { + type: 'object', + description: 'User statistics', + properties: { + followersCount: { type: 'number', description: 'Number of followers' }, + followingCount: { type: 'number', description: 'Number of users following' }, + tweetCount: { type: 'number', description: 'Total number of tweets' }, + }, + }, + }, + }, + }, + request: { url: (params) => { const username = encodeURIComponent(params.username) diff --git a/apps/sim/tools/x/write.ts b/apps/sim/tools/x/write.ts index c509c33fc8..2c68789f5c 100644 --- a/apps/sim/tools/x/write.ts +++ b/apps/sim/tools/x/write.ts @@ -46,6 +46,29 @@ export const xWriteTool: ToolConfig = { }, }, + outputs: { + tweet: { + type: 'object', + description: 'The newly created tweet data', + properties: { + id: { type: 'string', description: 'Tweet ID' }, + text: { type: 'string', description: 'Tweet content text' }, + createdAt: { type: 'string', description: 'Tweet creation timestamp' }, + authorId: { type: 'string', description: 'ID of the tweet author' }, + conversationId: { type: 'string', description: 'Conversation thread ID', optional: true }, + attachments: { + type: 'object', + description: 'Media or poll attachments', + optional: true, + properties: { + mediaKeys: { type: 'array', description: 'Media attachment keys', optional: true }, + pollId: { type: 'string', description: 'Poll ID if poll attached', optional: true }, + }, + }, + }, + }, + }, + request: { url: 'https://api.twitter.com/2/tweets', method: 'POST', diff --git a/apps/sim/tools/youtube/search.ts b/apps/sim/tools/youtube/search.ts index bd910278c8..1f2ca42e7a 100644 --- a/apps/sim/tools/youtube/search.ts +++ b/apps/sim/tools/youtube/search.ts @@ -27,6 +27,32 @@ export const youtubeSearchTool: ToolConfig { let url = `https://www.googleapis.com/youtube/v3/search?part=snippet&type=video&key=${params.apiKey}&q=${encodeURIComponent( diff --git a/apps/sim/trigger.config.ts b/apps/sim/trigger.config.ts index 33d5c3d2d0..9e166d9eed 100644 --- a/apps/sim/trigger.config.ts +++ b/apps/sim/trigger.config.ts @@ -11,5 +11,5 @@ export default defineConfig({ maxAttempts: 1, }, }, - dirs: ['./trigger'], + dirs: ['./background'], }) diff --git a/apps/sim/triggers/airtable/index.ts b/apps/sim/triggers/airtable/index.ts new file mode 100644 index 0000000000..ba2bc49df8 --- /dev/null +++ b/apps/sim/triggers/airtable/index.ts @@ -0,0 +1 @@ +export { airtableWebhookTrigger } from './webhook' diff --git a/apps/sim/triggers/airtable/webhook.ts b/apps/sim/triggers/airtable/webhook.ts new file mode 100644 index 0000000000..bae9be80cd --- /dev/null +++ b/apps/sim/triggers/airtable/webhook.ts @@ -0,0 +1,125 @@ +import { AirtableIcon } from '@/components/icons' +import type { TriggerConfig } from '../types' + +export const airtableWebhookTrigger: TriggerConfig = { + id: 'airtable_webhook', + name: 'Airtable Webhook', + provider: 'airtable', + description: + 'Trigger workflow from Airtable record changes like create, update, and delete events', + version: '1.0.0', + icon: AirtableIcon, + + configFields: { + baseId: { + type: 'string', + label: 'Base ID', + placeholder: 'appXXXXXXXXXXXXXX', + description: 'The ID of the Airtable Base this webhook will monitor.', + required: true, + }, + tableId: { + type: 'string', + label: 'Table ID', + placeholder: 'tblXXXXXXXXXXXXXX', + description: 'The ID of the table within the Base that the webhook will monitor.', + required: true, + }, + includeCellValues: { + type: 'boolean', + label: 'Include Full Record Data', + description: 'Enable to receive the complete record data in the payload, not just changes.', + defaultValue: false, + }, + }, + + outputs: { + event_type: { + type: 'string', + description: 'Type of Airtable event (e.g., record.created, record.updated, record.deleted)', + }, + base_id: { + type: 'string', + description: 'Airtable base identifier', + }, + table_id: { + type: 'string', + description: 'Airtable table identifier', + }, + record_id: { + type: 'string', + description: 'Record identifier that was modified', + }, + record_data: { + type: 'string', + description: 'Complete record data (when Include Full Record Data is enabled)', + }, + changed_fields: { + type: 'string', + description: 'Fields that were changed in the record', + }, + webhook_id: { + type: 'string', + description: 'Unique webhook identifier', + }, + timestamp: { + type: 'string', + description: 'Event timestamp', + }, + }, + + instructions: [ + 'Ensure you have provided the correct Base ID and Table ID above.', + 'Sim will automatically configure the webhook in your Airtable account when you save.', + 'Any changes made to records in the specified table will trigger this workflow.', + "If 'Include Full Record Data' is enabled, the entire record will be sent; otherwise, only the changed fields are sent.", + 'You can find your Base ID in the Airtable URL or API documentation for your base.', + 'Table IDs can be found in the Airtable API documentation or by inspecting the table URL.', + ], + + samplePayload: { + webhook: { + id: 'achAbCdEfGhIjKlMn', + }, + timestamp: '2023-01-01T00:00:00.000Z', + base: { + id: 'appXXXXXXXXXXXXXX', + }, + table: { + id: 'tblXXXXXXXXXXXXXX', + }, + changedTablesById: { + tblXXXXXXXXXXXXXX: { + changedRecordsById: { + recXXXXXXXXXXXXXX: { + current: { + id: 'recXXXXXXXXXXXXXX', + createdTime: '2023-01-01T00:00:00.000Z', + fields: { + Name: 'Sample Record', + Status: 'Active', + }, + }, + previous: { + id: 'recXXXXXXXXXXXXXX', + createdTime: '2023-01-01T00:00:00.000Z', + fields: { + Name: 'Sample Record', + Status: 'Inactive', + }, + }, + }, + }, + createdRecordsById: {}, + destroyedRecordIds: [], + }, + }, + }, + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }, +} diff --git a/apps/sim/triggers/discord/index.ts b/apps/sim/triggers/discord/index.ts new file mode 100644 index 0000000000..49d8a1889d --- /dev/null +++ b/apps/sim/triggers/discord/index.ts @@ -0,0 +1 @@ +export { discordWebhookTrigger } from './webhook' diff --git a/apps/sim/triggers/discord/webhook.ts b/apps/sim/triggers/discord/webhook.ts new file mode 100644 index 0000000000..7474e44900 --- /dev/null +++ b/apps/sim/triggers/discord/webhook.ts @@ -0,0 +1,95 @@ +import { DiscordIcon } from '@/components/icons' +import type { TriggerConfig } from '../types' + +export const discordWebhookTrigger: TriggerConfig = { + id: 'discord_webhook', + name: 'Discord Webhook', + provider: 'discord', + description: 'Trigger workflow from Discord webhook events and send messages to Discord channels', + version: '1.0.0', + icon: DiscordIcon, + + configFields: { + webhookName: { + type: 'string', + label: 'Webhook Name', + placeholder: 'Sim Bot', + description: 'This name will be displayed as the sender of messages in Discord.', + required: false, + }, + avatarUrl: { + type: 'string', + label: 'Avatar URL', + placeholder: 'https://example.com/avatar.png', + description: "URL to an image that will be used as the webhook's avatar.", + required: false, + }, + }, + + outputs: { + content: { + type: 'string', + description: 'Message content from Discord webhook', + }, + username: { + type: 'string', + description: 'Username of the sender (if provided)', + }, + avatar_url: { + type: 'string', + description: 'Avatar URL of the sender (if provided)', + }, + timestamp: { + type: 'string', + description: 'Timestamp when the webhook was triggered', + }, + webhook_id: { + type: 'string', + description: 'Discord webhook identifier', + }, + webhook_token: { + type: 'string', + description: 'Discord webhook token', + }, + guild_id: { + type: 'string', + description: 'Discord server/guild ID', + }, + channel_id: { + type: 'string', + description: 'Discord channel ID where the event occurred', + }, + embeds: { + type: 'string', + description: 'Embedded content data (if any)', + }, + }, + + instructions: [ + 'Go to Discord Server Settings > Integrations.', + 'Click "Webhooks" then "New Webhook".', + 'Customize the name and channel.', + 'Click "Copy Webhook URL".', + 'Paste the copied Discord URL into the main Webhook URL field above.', + 'Your workflow triggers when Discord sends an event to that URL.', + ], + + samplePayload: { + content: 'Hello from Sim!', + username: 'Optional Custom Name', + avatar_url: 'https://example.com/avatar.png', + timestamp: new Date().toISOString(), + webhook_id: '1234567890123456789', + webhook_token: 'example-webhook-token', + guild_id: '0987654321098765432', + channel_id: '1122334455667788990', + embeds: [], + }, + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }, +} diff --git a/apps/sim/triggers/generic/index.ts b/apps/sim/triggers/generic/index.ts new file mode 100644 index 0000000000..88f16d54b2 --- /dev/null +++ b/apps/sim/triggers/generic/index.ts @@ -0,0 +1 @@ +export { genericWebhookTrigger } from './webhook' diff --git a/apps/sim/triggers/generic/webhook.ts b/apps/sim/triggers/generic/webhook.ts new file mode 100644 index 0000000000..654da44942 --- /dev/null +++ b/apps/sim/triggers/generic/webhook.ts @@ -0,0 +1,84 @@ +import { WebhookIcon } from '@/components/icons' +import type { TriggerConfig } from '../types' + +export const genericWebhookTrigger: TriggerConfig = { + id: 'generic_webhook', + name: 'Generic Webhook', + provider: 'generic', + description: 'Receive webhooks from any service or API', + version: '1.0.0', + icon: WebhookIcon, + + configFields: { + // Generic webhooks don't require any specific configuration + // The webhook URL is provided automatically + }, + + outputs: { + payload: { + type: 'json', + description: 'Complete webhook payload received', + }, + headers: { + type: 'json', + description: 'HTTP request headers', + }, + method: { + type: 'string', + description: 'HTTP method (GET, POST, PUT, etc.)', + }, + url: { + type: 'string', + description: 'Request URL path', + }, + query: { + type: 'json', + description: 'URL query parameters', + }, + timestamp: { + type: 'string', + description: 'Webhook received timestamp', + }, + // Common fields that many services use + event: { + type: 'string', + description: 'Event type (extracted from payload.event, payload.type, or payload.event_type)', + }, + id: { + type: 'string', + description: 'Event ID (extracted from payload.id, payload.event_id, or payload.uuid)', + }, + data: { + type: 'json', + description: 'Event data (extracted from payload.data or the full payload)', + }, + }, + + instructions: [ + 'Copy the webhook URL provided above and use it in your external service or API.', + 'Configure your service to send webhooks to this URL.', + 'The webhook will receive any HTTP method (GET, POST, PUT, DELETE, etc.).', + 'All request data (headers, body, query parameters) will be available in your workflow.', + 'Common fields like "event", "id", and "data" will be automatically extracted from the payload when available.', + ], + + samplePayload: { + event: 'user.created', + id: 'evt_1234567890', + data: { + user: { + id: 'user_123', + email: 'user@example.com', + name: 'John Doe', + }, + }, + timestamp: '2023-01-01T12:00:00Z', + }, + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }, +} diff --git a/apps/sim/triggers/github/index.ts b/apps/sim/triggers/github/index.ts new file mode 100644 index 0000000000..3466962ad8 --- /dev/null +++ b/apps/sim/triggers/github/index.ts @@ -0,0 +1 @@ +export { githubWebhookTrigger } from './webhook' diff --git a/apps/sim/triggers/github/webhook.ts b/apps/sim/triggers/github/webhook.ts new file mode 100644 index 0000000000..93de1df336 --- /dev/null +++ b/apps/sim/triggers/github/webhook.ts @@ -0,0 +1,166 @@ +import { GithubIcon } from '@/components/icons' +import type { TriggerConfig } from '../types' + +export const githubWebhookTrigger: TriggerConfig = { + id: 'github_webhook', + name: 'GitHub Webhook', + provider: 'github', + description: 'Trigger workflow from GitHub events like push, pull requests, issues, and more', + version: '1.0.0', + icon: GithubIcon, + + configFields: { + contentType: { + type: 'select', + label: 'Content Type', + options: ['application/json', 'application/x-www-form-urlencoded'], + defaultValue: 'application/json', + description: 'Format GitHub will use when sending the webhook payload.', + required: true, + }, + webhookSecret: { + type: 'string', + label: 'Webhook Secret (Recommended)', + placeholder: 'Generate or enter a strong secret', + description: 'Validates that webhook deliveries originate from GitHub.', + required: false, + isSecret: true, + }, + sslVerification: { + type: 'select', + label: 'SSL Verification', + options: ['enabled', 'disabled'], + defaultValue: 'enabled', + description: 'GitHub verifies SSL certificates when delivering webhooks.', + required: true, + }, + }, + + outputs: { + action: { + type: 'string', + description: 'The action that was performed (e.g., opened, closed, synchronize)', + }, + event_type: { + type: 'string', + description: 'Type of GitHub event (e.g., push, pull_request, issues)', + }, + repository: { + type: 'string', + description: 'Repository full name (owner/repo)', + }, + repository_name: { + type: 'string', + description: 'Repository name only', + }, + repository_owner: { + type: 'string', + description: 'Repository owner username or organization', + }, + sender: { + type: 'string', + description: 'Username of the user who triggered the event', + }, + sender_id: { + type: 'string', + description: 'User ID of the sender', + }, + ref: { + type: 'string', + description: 'Git reference (for push events)', + }, + before: { + type: 'string', + description: 'SHA of the commit before the push', + }, + after: { + type: 'string', + description: 'SHA of the commit after the push', + }, + commits: { + type: 'string', + description: 'Array of commit objects (for push events)', + }, + pull_request: { + type: 'string', + description: 'Pull request object (for pull_request events)', + }, + issue: { + type: 'string', + description: 'Issue object (for issues events)', + }, + comment: { + type: 'string', + description: 'Comment object (for comment events)', + }, + branch: { + type: 'string', + description: 'Branch name extracted from ref', + }, + commit_message: { + type: 'string', + description: 'Latest commit message', + }, + commit_author: { + type: 'string', + description: 'Author of the latest commit', + }, + }, + + instructions: [ + 'Go to your GitHub Repository > Settings > Webhooks.', + 'Click "Add webhook".', + 'Paste the Webhook URL (from above) into the "Payload URL" field.', + 'Select your chosen Content Type from the dropdown above.', + 'Enter the Webhook Secret (from above) into the "Secret" field if you\'ve configured one.', + 'Set SSL verification according to your selection above.', + 'Choose which events should trigger this webhook.', + 'Ensure "Active" is checked and click "Add webhook".', + ], + + samplePayload: { + action: 'opened', + number: 1, + pull_request: { + id: 1, + number: 1, + state: 'open', + title: 'Update README', + user: { + login: 'octocat', + id: 1, + }, + body: 'This is a pretty simple change that we need to pull into main.', + head: { + ref: 'feature-branch', + sha: 'abc123', + }, + base: { + ref: 'main', + sha: 'def456', + }, + }, + repository: { + id: 35129377, + name: 'public-repo', + full_name: 'baxterthehacker/public-repo', + owner: { + login: 'baxterthehacker', + id: 6752317, + }, + }, + sender: { + login: 'baxterthehacker', + id: 6752317, + }, + }, + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-GitHub-Event': 'pull_request', + 'X-GitHub-Delivery': 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx', + }, + }, +} diff --git a/apps/sim/triggers/gmail/index.ts b/apps/sim/triggers/gmail/index.ts new file mode 100644 index 0000000000..1724af8152 --- /dev/null +++ b/apps/sim/triggers/gmail/index.ts @@ -0,0 +1 @@ +export { gmailPollingTrigger } from './poller' diff --git a/apps/sim/triggers/gmail/poller.ts b/apps/sim/triggers/gmail/poller.ts new file mode 100644 index 0000000000..4301b6f9a2 --- /dev/null +++ b/apps/sim/triggers/gmail/poller.ts @@ -0,0 +1,142 @@ +import { GmailIcon } from '@/components/icons' +import type { TriggerConfig } from '@/triggers/types' + +export const gmailPollingTrigger: TriggerConfig = { + id: 'gmail_poller', + name: 'Gmail Email Trigger', + provider: 'gmail', + description: 'Triggers when new emails are received in Gmail (requires Gmail credentials)', + version: '1.0.0', + icon: GmailIcon, + + // Gmail requires OAuth credentials to work + requiresCredentials: true, + credentialProvider: 'google-email', + + configFields: { + labelIds: { + type: 'multiselect', + label: 'Gmail Labels to Monitor', + placeholder: 'Select Gmail labels to monitor for new emails', + description: 'Choose which Gmail labels to monitor. Leave empty to monitor all emails.', + required: false, + options: [], // Will be populated dynamically from user's Gmail labels + }, + labelFilterBehavior: { + type: 'select', + label: 'Label Filter Behavior', + options: ['INCLUDE', 'EXCLUDE'], + defaultValue: 'INCLUDE', + description: + 'Include only emails with selected labels, or exclude emails with selected labels', + required: true, + }, + markAsRead: { + type: 'boolean', + label: 'Mark as Read', + defaultValue: false, + description: 'Automatically mark emails as read after processing', + required: false, + }, + includeRawEmail: { + type: 'boolean', + label: 'Include Raw Email Data', + defaultValue: false, + description: 'Include the complete raw Gmail API response in the trigger payload', + required: false, + }, + }, + + outputs: { + email: { + id: { + type: 'string', + description: 'Gmail message ID', + }, + threadId: { + type: 'string', + description: 'Gmail thread ID', + }, + subject: { + type: 'string', + description: 'Email subject line', + }, + from: { + type: 'string', + description: 'Sender email address', + }, + to: { + type: 'string', + description: 'Recipient email address', + }, + cc: { + type: 'string', + description: 'CC recipients', + }, + date: { + type: 'string', + description: 'Email date in ISO format', + }, + bodyText: { + type: 'string', + description: 'Plain text email body', + }, + bodyHtml: { + type: 'string', + description: 'HTML email body', + }, + labels: { + type: 'string', + description: 'Email labels array', + }, + hasAttachments: { + type: 'boolean', + description: 'Whether email has attachments', + }, + attachments: { + type: 'json', + description: 'Array of attachment information', + }, + }, + timestamp: { + type: 'string', + description: 'Event timestamp', + }, + rawEmail: { + type: 'json', + description: 'Complete raw email data from Gmail API (if enabled)', + }, + }, + + instructions: [ + 'Connect your Gmail account using OAuth credentials', + 'Configure which Gmail labels to monitor (optional)', + 'The system will automatically check for new emails and trigger your workflow', + ], + + samplePayload: { + email: { + id: '18e0ffabd5b5a0f4', + threadId: '18e0ffabd5b5a0f4', + subject: 'Monthly Report - April 2025', + from: 'sender@example.com', + to: 'recipient@example.com', + cc: 'team@example.com', + date: '2025-05-10T10:15:23.000Z', + bodyText: + 'Hello,\n\nPlease find attached the monthly report for April 2025.\n\nBest regards,\nSender', + bodyHtml: + '

Hello,

Please find attached the monthly report for April 2025.

Best regards,
Sender

', + labels: ['INBOX', 'IMPORTANT'], + hasAttachments: true, + attachments: [ + { + filename: 'report-april-2025.pdf', + mimeType: 'application/pdf', + size: 2048576, + }, + ], + }, + timestamp: '2025-05-10T10:15:30.123Z', + }, +} diff --git a/apps/sim/triggers/index.ts b/apps/sim/triggers/index.ts new file mode 100644 index 0000000000..3bb7493a44 --- /dev/null +++ b/apps/sim/triggers/index.ts @@ -0,0 +1,53 @@ +// Import trigger definitions + +import { airtableWebhookTrigger } from './airtable' +import { discordWebhookTrigger } from './discord' +import { genericWebhookTrigger } from './generic' +import { githubWebhookTrigger } from './github' +import { gmailPollingTrigger } from './gmail' +import { microsoftTeamsWebhookTrigger } from './microsoftteams' +import { outlookPollingTrigger } from './outlook' +import { slackWebhookTrigger } from './slack' +import { stripeWebhookTrigger } from './stripe/webhook' +import { telegramWebhookTrigger } from './telegram' +import type { TriggerConfig, TriggerRegistry } from './types' +import { whatsappWebhookTrigger } from './whatsapp' + +// Central registry of all available triggers +export const TRIGGER_REGISTRY: TriggerRegistry = { + slack_webhook: slackWebhookTrigger, + airtable_webhook: airtableWebhookTrigger, + discord_webhook: discordWebhookTrigger, + generic_webhook: genericWebhookTrigger, + github_webhook: githubWebhookTrigger, + gmail_poller: gmailPollingTrigger, + microsoftteams_webhook: microsoftTeamsWebhookTrigger, + outlook_poller: outlookPollingTrigger, + stripe_webhook: stripeWebhookTrigger, + telegram_webhook: telegramWebhookTrigger, + whatsapp_webhook: whatsappWebhookTrigger, +} + +// Utility functions for working with triggers +export function getTrigger(triggerId: string): TriggerConfig | undefined { + return TRIGGER_REGISTRY[triggerId] +} + +export function getTriggersByProvider(provider: string): TriggerConfig[] { + return Object.values(TRIGGER_REGISTRY).filter((trigger) => trigger.provider === provider) +} + +export function getAllTriggers(): TriggerConfig[] { + return Object.values(TRIGGER_REGISTRY) +} + +export function getTriggerIds(): string[] { + return Object.keys(TRIGGER_REGISTRY) +} + +export function isTriggerValid(triggerId: string): boolean { + return triggerId in TRIGGER_REGISTRY +} + +// Export types for use elsewhere +export type { TriggerConfig, TriggerRegistry } from './types' diff --git a/apps/sim/triggers/microsoftteams/index.ts b/apps/sim/triggers/microsoftteams/index.ts new file mode 100644 index 0000000000..e9cfa2876f --- /dev/null +++ b/apps/sim/triggers/microsoftteams/index.ts @@ -0,0 +1 @@ +export { microsoftTeamsWebhookTrigger } from './webhook' diff --git a/apps/sim/triggers/microsoftteams/webhook.ts b/apps/sim/triggers/microsoftteams/webhook.ts new file mode 100644 index 0000000000..598e3e8d2a --- /dev/null +++ b/apps/sim/triggers/microsoftteams/webhook.ts @@ -0,0 +1,100 @@ +import { MicrosoftTeamsIcon } from '@/components/icons' +import type { TriggerConfig } from '../types' + +export const microsoftTeamsWebhookTrigger: TriggerConfig = { + id: 'microsoftteams_webhook', + name: 'Microsoft Teams Webhook', + provider: 'microsoftteams', + description: 'Trigger workflow from Microsoft Teams events like messages and mentions', + version: '1.0.0', + icon: MicrosoftTeamsIcon, + + configFields: { + hmacSecret: { + type: 'string', + label: 'HMAC Secret', + placeholder: 'Enter HMAC secret from Teams', + description: + 'The security token provided by Teams when creating an outgoing webhook. Used to verify request authenticity.', + required: true, + isSecret: true, + }, + }, + + outputs: { + type: { + type: 'string', + description: 'Type of Teams message (e.g., message)', + }, + id: { + type: 'string', + description: 'Unique message identifier', + }, + timestamp: { + type: 'string', + description: 'Message timestamp', + }, + localTimestamp: { + type: 'string', + description: 'Local timestamp of the message', + }, + serviceUrl: { + type: 'string', + description: 'Microsoft Teams service URL', + }, + channelId: { + type: 'string', + description: 'Teams channel ID where the event occurred', + }, + from_id: { + type: 'string', + description: 'User ID who sent the message', + }, + from_name: { + type: 'string', + description: 'Username who sent the message', + }, + conversation_id: { + type: 'string', + description: 'Conversation/thread ID', + }, + text: { + type: 'string', + description: 'Message text content', + }, + }, + + instructions: [ + 'Open Microsoft Teams and go to the team where you want to add the webhook.', + 'Click the three dots (•••) next to the team name and select "Manage team".', + 'Go to the "Apps" tab and click "Create an outgoing webhook".', + 'Provide a name, description, and optionally a profile picture.', + 'Set the callback URL to your Sim webhook URL (shown above).', + 'Copy the HMAC security token and paste it into the "HMAC Secret" field above.', + 'Click "Create" to finish setup.', + ], + + samplePayload: { + type: 'message', + id: '1234567890', + timestamp: '2023-01-01T00:00:00.000Z', + localTimestamp: '2023-01-01T00:00:00.000Z', + serviceUrl: 'https://smba.trafficmanager.net/amer/', + channelId: 'msteams', + from: { + id: '29:1234567890abcdef', + name: 'John Doe', + }, + conversation: { + id: '19:meeting_abcdef@thread.v2', + }, + text: 'Hello Sim Bot!', + }, + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }, +} diff --git a/apps/sim/triggers/outlook/index.ts b/apps/sim/triggers/outlook/index.ts new file mode 100644 index 0000000000..5fca542b25 --- /dev/null +++ b/apps/sim/triggers/outlook/index.ts @@ -0,0 +1 @@ +export { outlookPollingTrigger } from './poller' diff --git a/apps/sim/triggers/outlook/poller.ts b/apps/sim/triggers/outlook/poller.ts new file mode 100644 index 0000000000..5356f89b2f --- /dev/null +++ b/apps/sim/triggers/outlook/poller.ts @@ -0,0 +1,146 @@ +import { OutlookIcon } from '@/components/icons' +import type { TriggerConfig } from '@/triggers/types' + +export const outlookPollingTrigger: TriggerConfig = { + id: 'outlook_poller', + name: 'Outlook Email Trigger', + provider: 'outlook', + description: 'Triggers when new emails are received in Outlook (requires Microsoft credentials)', + version: '1.0.0', + icon: OutlookIcon, + + // Outlook requires OAuth credentials to work + requiresCredentials: true, + credentialProvider: 'outlook', + + configFields: { + folderIds: { + type: 'multiselect', + label: 'Outlook Folders to Monitor', + placeholder: 'Select Outlook folders to monitor for new emails', + description: 'Choose which Outlook folders to monitor. Leave empty to monitor all emails.', + required: false, + options: [], // Will be populated dynamically from user's Outlook folders + }, + folderFilterBehavior: { + type: 'select', + label: 'Folder Filter Behavior', + options: ['INCLUDE', 'EXCLUDE'], + defaultValue: 'INCLUDE', + description: + 'Include only emails from selected folders, or exclude emails from selected folders', + required: true, + }, + markAsRead: { + type: 'boolean', + label: 'Mark as Read', + defaultValue: false, + description: 'Automatically mark emails as read after processing', + required: false, + }, + includeRawEmail: { + type: 'boolean', + label: 'Include Raw Email Data', + defaultValue: false, + description: 'Include the complete raw Microsoft Graph API response in the trigger payload', + required: false, + }, + }, + + outputs: { + email: { + id: { + type: 'string', + description: 'Outlook message ID', + }, + conversationId: { + type: 'string', + description: 'Outlook conversation ID', + }, + subject: { + type: 'string', + description: 'Email subject line', + }, + from: { + type: 'string', + description: 'Sender email address', + }, + to: { + type: 'string', + description: 'Recipient email address', + }, + cc: { + type: 'string', + description: 'CC recipients', + }, + date: { + type: 'string', + description: 'Email date in ISO format', + }, + bodyText: { + type: 'string', + description: 'Plain text email body (preview)', + }, + bodyHtml: { + type: 'string', + description: 'HTML email body', + }, + hasAttachments: { + type: 'boolean', + description: 'Whether email has attachments', + }, + isRead: { + type: 'boolean', + description: 'Whether email is read', + }, + folderId: { + type: 'string', + description: 'Outlook folder ID where email is located', + }, + messageId: { + type: 'string', + description: 'Message ID for threading', + }, + threadId: { + type: 'string', + description: 'Thread ID for conversation threading', + }, + }, + timestamp: { + type: 'string', + description: 'Event timestamp', + }, + rawEmail: { + type: 'json', + description: 'Complete raw email data from Microsoft Graph API (if enabled)', + }, + }, + + instructions: [ + 'Connect your Microsoft account using OAuth credentials', + 'Configure which Outlook folders to monitor (optional)', + 'The system will automatically check for new emails and trigger your workflow', + ], + + samplePayload: { + email: { + id: 'AAMkADg1OWUyZjg4LWJkNGYtNDFhYy04OGVjLWVkM2VhY2YzYTcwZgBGAAAAAACE3bU', + conversationId: 'AAQkADg1OWUyZjg4LWJkNGYtNDFhYy04OGVjLWVkM2VhY2YzYTcwZgAQAErzGBJV', + subject: 'Quarterly Business Review - Q1 2025', + from: 'manager@company.com', + to: 'team@company.com', + cc: 'stakeholders@company.com', + date: '2025-05-10T14:30:00Z', + bodyText: + 'Hi Team,\n\nPlease find attached the Q1 2025 business review document. We need to discuss the results in our next meeting.\n\nBest regards,\nManager', + bodyHtml: + '

Hi Team,

Please find attached the Q1 2025 business review document. We need to discuss the results in our next meeting.

Best regards,
Manager

', + hasAttachments: true, + isRead: false, + folderId: 'AQMkADg1OWUyZjg4LWJkNGYtNDFhYy04OGVjAC4AAAJzE3bU', + messageId: 'AAMkADg1OWUyZjg4LWJkNGYtNDFhYy04OGVjLWVkM2VhY2YzYTcwZgBGAAAAAACE3bU', + threadId: 'AAQkADg1OWUyZjg4LWJkNGYtNDFhYy04OGVjLWVkM2VhY2YzYTcwZgAQAErzGBJV', + }, + timestamp: '2025-05-10T14:30:15.123Z', + }, +} diff --git a/apps/sim/triggers/slack/index.ts b/apps/sim/triggers/slack/index.ts new file mode 100644 index 0000000000..93eee192e0 --- /dev/null +++ b/apps/sim/triggers/slack/index.ts @@ -0,0 +1 @@ +export { slackWebhookTrigger } from './webhook' diff --git a/apps/sim/triggers/slack/webhook.ts b/apps/sim/triggers/slack/webhook.ts new file mode 100644 index 0000000000..fe652b6901 --- /dev/null +++ b/apps/sim/triggers/slack/webhook.ts @@ -0,0 +1,92 @@ +import { SlackIcon } from '@/components/icons' +import type { TriggerConfig } from '../types' + +export const slackWebhookTrigger: TriggerConfig = { + id: 'slack_webhook', + name: 'Slack Webhook', + provider: 'slack', + description: 'Trigger workflow from Slack events like mentions, messages, and reactions', + version: '1.0.0', + icon: SlackIcon, + + configFields: { + signingSecret: { + type: 'string', + label: 'Signing Secret', + placeholder: 'Enter your Slack app signing secret', + description: 'The signing secret from your Slack app to validate request authenticity.', + required: true, + isSecret: true, + }, + }, + + outputs: { + event_type: { + type: 'string', + description: 'Type of Slack event (e.g., app_mention, message)', + }, + channel: { + type: 'string', + description: 'Slack channel ID where the event occurred', + }, + channel_name: { + type: 'string', + description: 'Human-readable channel name', + }, + user: { + type: 'string', + description: 'User ID who triggered the event', + }, + user_name: { + type: 'string', + description: 'Username who triggered the event', + }, + text: { + type: 'string', + description: 'Message text content', + }, + timestamp: { + type: 'string', + description: 'Event timestamp', + }, + team_id: { + type: 'string', + description: 'Slack workspace/team ID', + }, + event_id: { + type: 'string', + description: 'Unique event identifier', + }, + }, + + instructions: [ + 'Go to Slack Apps page', + 'If you don\'t have an app:
  • Create an app from scratch
  • Give it a name and select your workspace
', + 'Go to "Basic Information", find the "Signing Secret", and paste it in the field above.', + 'Go to "OAuth & Permissions" and add bot token scopes:
  • app_mentions:read - For viewing messages that tag your bot with an @
  • chat:write - To send messages to channels your bot is a part of
', + 'Go to "Event Subscriptions":
  • Enable events
  • Under "Subscribe to Bot Events", add app_mention to listen to messages that mention your bot
  • Paste the Webhook URL (from above) into the "Request URL" field
', + 'Save changes in both Slack and here.', + ], + + samplePayload: { + type: 'event_callback', + event: { + type: 'app_mention', + channel: 'C0123456789', + user: 'U0123456789', + text: '<@U0BOTUSER123> Hello from Slack!', + ts: '1234567890.123456', + channel_type: 'channel', + }, + team_id: 'T0123456789', + event_id: 'Ev0123456789', + event_time: 1234567890, + }, + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }, +} diff --git a/apps/sim/triggers/stripe/index.ts b/apps/sim/triggers/stripe/index.ts new file mode 100644 index 0000000000..3a0b14e6cf --- /dev/null +++ b/apps/sim/triggers/stripe/index.ts @@ -0,0 +1 @@ +export { stripeWebhookTrigger } from './webhook' diff --git a/apps/sim/triggers/stripe/webhook.ts b/apps/sim/triggers/stripe/webhook.ts new file mode 100644 index 0000000000..fb9ca0a4a4 --- /dev/null +++ b/apps/sim/triggers/stripe/webhook.ts @@ -0,0 +1,93 @@ +import { ShieldCheck } from 'lucide-react' +import type { TriggerConfig } from '../types' + +export const stripeWebhookTrigger: TriggerConfig = { + id: 'stripe_webhook', + name: 'Stripe Webhook', + provider: 'stripe', + description: 'Triggers when Stripe events occur (payments, subscriptions, etc.)', + version: '1.0.0', + icon: ShieldCheck, + + configFields: { + // Stripe webhooks don't require configuration fields - events are selected in Stripe dashboard + }, + + outputs: { + id: { + type: 'string', + description: 'Event ID from Stripe', + }, + type: { + type: 'string', + description: 'Event type (e.g., charge.succeeded, payment_intent.succeeded)', + }, + created: { + type: 'string', + description: 'Timestamp when the event was created', + }, + data: { + type: 'string', + description: 'Event data containing the affected Stripe object', + }, + object: { + type: 'string', + description: 'The Stripe object that was updated (e.g., charge, payment_intent)', + }, + livemode: { + type: 'string', + description: 'Whether this event occurred in live mode or test mode', + }, + apiVersion: { + type: 'string', + description: 'API version used to render this event', + }, + request: { + type: 'string', + description: 'Information about the request that triggered this event', + }, + }, + + instructions: [ + 'Go to your Stripe Dashboard at https://dashboard.stripe.com/', + 'Navigate to Developers > Webhooks', + 'Click "Add endpoint"', + 'Paste the Webhook URL (from above) into the "Endpoint URL" field', + 'Select the events you want to listen to (e.g., charge.succeeded)', + 'Click "Add endpoint"', + 'Stripe will send a test event to verify your webhook endpoint', + ], + + samplePayload: { + id: 'evt_1234567890', + type: 'charge.succeeded', + created: 1641234567, + data: { + object: { + id: 'ch_1234567890', + object: 'charge', + amount: 2500, + currency: 'usd', + description: 'Sample charge', + paid: true, + status: 'succeeded', + customer: 'cus_1234567890', + receipt_email: 'customer@example.com', + }, + }, + object: 'event', + livemode: false, + api_version: '2020-08-27', + request: { + id: 'req_1234567890', + idempotency_key: null, + }, + }, + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }, +} diff --git a/apps/sim/triggers/telegram/index.ts b/apps/sim/triggers/telegram/index.ts new file mode 100644 index 0000000000..72ac27a808 --- /dev/null +++ b/apps/sim/triggers/telegram/index.ts @@ -0,0 +1 @@ +export { telegramWebhookTrigger } from './webhook' diff --git a/apps/sim/triggers/telegram/webhook.ts b/apps/sim/triggers/telegram/webhook.ts new file mode 100644 index 0000000000..01afbc13f2 --- /dev/null +++ b/apps/sim/triggers/telegram/webhook.ts @@ -0,0 +1,117 @@ +import { TelegramIcon } from '@/components/icons' +import type { TriggerConfig } from '../types' + +export const telegramWebhookTrigger: TriggerConfig = { + id: 'telegram_webhook', + name: 'Telegram Webhook', + provider: 'telegram', + description: 'Trigger workflow from Telegram bot messages and events', + version: '1.0.0', + icon: TelegramIcon, + + configFields: { + botToken: { + type: 'string', + label: 'Bot Token', + placeholder: '123456789:ABCdefGHIjklMNOpqrsTUVwxyz', + description: 'Your Telegram Bot Token from BotFather', + required: true, + isSecret: true, + }, + }, + + outputs: { + update_id: { + type: 'number', + description: 'Unique identifier for the update', + }, + message_id: { + type: 'number', + description: 'Unique message identifier', + }, + from_id: { + type: 'number', + description: 'User ID who sent the message', + }, + from_username: { + type: 'string', + description: 'Username of the sender', + }, + from_first_name: { + type: 'string', + description: 'First name of the sender', + }, + from_last_name: { + type: 'string', + description: 'Last name of the sender', + }, + chat_id: { + type: 'number', + description: 'Unique identifier for the chat', + }, + chat_type: { + type: 'string', + description: 'Type of chat (private, group, supergroup, channel)', + }, + chat_title: { + type: 'string', + description: 'Title of the chat (for groups and channels)', + }, + text: { + type: 'string', + description: 'Message text content', + }, + date: { + type: 'number', + description: 'Date the message was sent (Unix timestamp)', + }, + entities: { + type: 'string', + description: 'Special entities in the message (mentions, hashtags, etc.) as JSON string', + }, + }, + + instructions: [ + 'Message "/newbot" to @BotFather in Telegram to create a bot and copy its token.', + 'Enter your Bot Token above.', + 'Save settings and any message sent to your bot will trigger the workflow.', + ], + + samplePayload: { + update_id: 123456789, + message: { + message_id: 123, + from: { + id: 987654321, + is_bot: false, + first_name: 'John', + last_name: 'Doe', + username: 'johndoe', + language_code: 'en', + }, + chat: { + id: 987654321, + first_name: 'John', + last_name: 'Doe', + username: 'johndoe', + type: 'private', + }, + date: 1234567890, + text: 'Hello from Telegram!', + entities: [ + { + offset: 0, + length: 5, + type: 'bold', + }, + ], + }, + }, + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }, +} diff --git a/apps/sim/triggers/types.ts b/apps/sim/triggers/types.ts new file mode 100644 index 0000000000..7e54251aa4 --- /dev/null +++ b/apps/sim/triggers/types.ts @@ -0,0 +1,67 @@ +export type TriggerFieldType = 'string' | 'boolean' | 'select' | 'number' | 'multiselect' + +export interface TriggerConfigField { + type: TriggerFieldType + label: string + placeholder?: string + options?: string[] + defaultValue?: string | boolean | number | string[] + description?: string + required?: boolean + isSecret?: boolean +} + +export interface TriggerOutput { + type?: string + description?: string + [key: string]: TriggerOutput | string | undefined +} + +export interface TriggerConfig { + id: string + name: string + provider: string + description: string + version: string + + // Optional icon component for UI display + icon?: React.ComponentType<{ className?: string }> + + // Configuration fields that users need to fill + configFields: Record + + // Define the structure of data this trigger outputs to workflows + outputs: Record + + // Setup instructions for users + instructions: string[] + + // Example payload for documentation + samplePayload: any + + // Webhook configuration (for most triggers) + webhook?: { + method?: 'POST' | 'GET' | 'PUT' | 'DELETE' + headers?: Record + } + + // For triggers that require OAuth credentials (like Gmail) + requiresCredentials?: boolean + credentialProvider?: string // 'google-email', 'microsoft', etc. +} + +export interface TriggerRegistry { + [triggerId: string]: TriggerConfig +} + +export interface TriggerInstance { + id: string + triggerId: string + blockId: string + workflowId: string + config: Record + webhookPath?: string + isActive: boolean + createdAt: Date + updatedAt: Date +} diff --git a/apps/sim/triggers/whatsapp/index.ts b/apps/sim/triggers/whatsapp/index.ts new file mode 100644 index 0000000000..edc76eaa72 --- /dev/null +++ b/apps/sim/triggers/whatsapp/index.ts @@ -0,0 +1 @@ +export { whatsappWebhookTrigger } from './webhook' diff --git a/apps/sim/triggers/whatsapp/webhook.ts b/apps/sim/triggers/whatsapp/webhook.ts new file mode 100644 index 0000000000..583f220092 --- /dev/null +++ b/apps/sim/triggers/whatsapp/webhook.ts @@ -0,0 +1,108 @@ +import { WhatsAppIcon } from '@/components/icons' +import type { TriggerConfig } from '../types' + +export const whatsappWebhookTrigger: TriggerConfig = { + id: 'whatsapp_webhook', + name: 'WhatsApp Webhook', + provider: 'whatsapp', + description: 'Trigger workflow from WhatsApp messages and events via Business Platform webhooks', + version: '1.0.0', + icon: WhatsAppIcon, + + configFields: { + verificationToken: { + type: 'string', + label: 'Verification Token', + placeholder: 'Generate or enter a verification token', + description: + "Enter any secure token here. You'll need to provide the same token in your WhatsApp Business Platform dashboard.", + required: true, + isSecret: true, + }, + }, + + outputs: { + messageId: { + type: 'string', + description: 'Unique message identifier', + }, + from: { + type: 'string', + description: 'Phone number of the message sender', + }, + phoneNumberId: { + type: 'string', + description: 'WhatsApp Business phone number ID that received the message', + }, + text: { + type: 'string', + description: 'Message text content', + }, + timestamp: { + type: 'string', + description: 'Message timestamp', + }, + raw: { + type: 'string', + description: 'Complete raw message object from WhatsApp as JSON string', + }, + }, + + instructions: [ + 'Go to your Meta for Developers Apps page.', + 'If you don\'t have an app:
  • Create an app from scratch
  • Give it a name and select your workspace
', + 'Select your App, then navigate to WhatsApp > Configuration.', + 'Find the Webhooks section and click "Edit".', + 'Paste the Webhook URL (from above) into the "Callback URL" field.', + 'Paste the Verification Token (from above) into the "Verify token" field.', + 'Click "Verify and save".', + 'Click "Manage" next to Webhook fields and subscribe to `messages`.', + ], + + samplePayload: { + object: 'whatsapp_business_account', + entry: [ + { + id: '1234567890123456', + changes: [ + { + value: { + messaging_product: 'whatsapp', + metadata: { + display_phone_number: '15551234567', + phone_number_id: '1234567890123456', + }, + contacts: [ + { + profile: { + name: 'John Doe', + }, + wa_id: '15555551234', + }, + ], + messages: [ + { + from: '15555551234', + id: 'wamid.HBgNMTU1NTU1NTEyMzQVAgASGBQzQTdBNjg4QjU2NjZCMzY4ODE2AA==', + timestamp: '1234567890', + text: { + body: 'Hello from WhatsApp!', + }, + type: 'text', + }, + ], + }, + field: 'messages', + }, + ], + }, + ], + }, + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }, +} diff --git a/bun.lock b/bun.lock index 78bda3355a..24854fadae 100644 --- a/bun.lock +++ b/bun.lock @@ -162,7 +162,7 @@ "@types/js-yaml": "4.0.9", "@types/jsdom": "21.1.7", "@types/lodash": "^4.17.16", - "@types/node": "^22", + "@types/node": "24.2.1", "@types/prismjs": "^1.26.5", "@types/react": "^19", "@types/react-dom": "^19", @@ -3727,6 +3727,8 @@ "restore-cursor/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + "sim/@types/node": ["@types/node@24.2.1", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ=="], + "sim/lucide-react": ["lucide-react@0.479.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-aBhNnveRhorBOK7uA4gDjgaf+YlHMdMhQ/3cupk6exM10hWlEU+2QtWYOfhXhjAsmdb6LeKR+NZnow4UxRRiTQ=="], "sim/tailwind-merge": ["tailwind-merge@2.6.0", "", {}, "sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA=="], @@ -4081,6 +4083,8 @@ "restore-cursor/onetime/mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], + "sim/@types/node/undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], + "sim/tailwindcss/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], "sim/tailwindcss/jiti": ["jiti@1.21.7", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="], diff --git a/helm/sim/README.md b/helm/sim/README.md index d872a94534..fe75dd2342 100644 --- a/helm/sim/README.md +++ b/helm/sim/README.md @@ -12,20 +12,7 @@ This Helm chart deploys Sim, a lightweight AI agent workflow platform, on Kubern ### Quick Start -1. Add the chart repository (if using a separate repo): -```bash -helm repo add simstudio https://charts.sim.ai -helm repo update -``` - -2. Install the chart: -```bash -helm install sim simstudio/sim -``` - -### Local Installation - -If using the chart from this repository: +Install the chart from this repository: ```bash # From the repository root diff --git a/helm/sim/templates/deployment-app.yaml b/helm/sim/templates/deployment-app.yaml index 8923f335bf..4e064a8399 100644 --- a/helm/sim/templates/deployment-app.yaml +++ b/helm/sim/templates/deployment-app.yaml @@ -38,9 +38,10 @@ spec: - name: migrations image: {{ include "sim.image" (dict "context" . "image" .Values.migrations.image) }} imagePullPolicy: {{ .Values.migrations.image.pullPolicy }} - command: ["/bin/bash", "-c"] + command: ["/bin/sh", "-c"] args: - | + cd /app/apps/sim export DATABASE_URL="{{ include "sim.databaseUrl" . }}" bun run db:migrate {{- if .Values.postgresql.enabled }} @@ -67,10 +68,10 @@ spec: - name: DATABASE_URL value: {{ include "sim.databaseUrl" . | quote }} - name: SOCKET_SERVER_URL - value: {{ include "sim.socketServerUrl" . | quote }} + value: {{ .Values.app.env.SOCKET_SERVER_URL | default "http://localhost:3002" | quote }} - name: OLLAMA_URL value: {{ include "sim.ollamaUrl" . | quote }} - {{- range $key, $value := .Values.app.env }} + {{- range $key, $value := omit .Values.app.env "DATABASE_URL" "SOCKET_SERVER_URL" "OLLAMA_URL" }} - name: {{ $key }} value: {{ $value | quote }} {{- end }} diff --git a/helm/sim/values.schema.json b/helm/sim/values.schema.json index 9f7ea1ac86..b344664c97 100644 --- a/helm/sim/values.schema.json +++ b/helm/sim/values.schema.json @@ -113,6 +113,189 @@ "type": "string", "format": "uri", "description": "Public socket URL" + }, + "NODE_ENV": { + "type": "string", + "enum": ["development", "test", "production"], + "description": "Runtime environment" + }, + "NEXT_TELEMETRY_DISABLED": { + "type": "string", + "description": "Disable Next.js telemetry" + }, + "RESEND_API_KEY": { + "type": "string", + "description": "Resend API key for transactional emails" + }, + "EMAIL_DOMAIN": { + "type": "string", + "description": "Domain for sending emails" + }, + "GOOGLE_CLIENT_ID": { + "type": "string", + "description": "Google OAuth client ID" + }, + "GOOGLE_CLIENT_SECRET": { + "type": "string", + "description": "Google OAuth client secret" + }, + "GITHUB_CLIENT_ID": { + "type": "string", + "description": "GitHub OAuth client ID" + }, + "GITHUB_CLIENT_SECRET": { + "type": "string", + "description": "GitHub OAuth client secret" + }, + "OPENAI_API_KEY": { + "type": "string", + "description": "Primary OpenAI API key" + }, + "OPENAI_API_KEY_1": { + "type": "string", + "description": "Additional OpenAI API key for load balancing" + }, + "OPENAI_API_KEY_2": { + "type": "string", + "description": "Additional OpenAI API key for load balancing" + }, + "OPENAI_API_KEY_3": { + "type": "string", + "description": "Additional OpenAI API key for load balancing" + }, + "MISTRAL_API_KEY": { + "type": "string", + "description": "Mistral AI API key" + }, + "ANTHROPIC_API_KEY_1": { + "type": "string", + "description": "Primary Anthropic Claude API key" + }, + "ANTHROPIC_API_KEY_2": { + "type": "string", + "description": "Additional Anthropic API key for load balancing" + }, + "ANTHROPIC_API_KEY_3": { + "type": "string", + "description": "Additional Anthropic API key for load balancing" + }, + "OLLAMA_URL": { + "type": "string", + "format": "uri", + "description": "Ollama local LLM server URL" + }, + "ELEVENLABS_API_KEY": { + "type": "string", + "description": "ElevenLabs API key for text-to-speech in deployed chat" + }, + "RATE_LIMIT_WINDOW_MS": { + "type": "string", + "description": "Rate limit window duration in milliseconds" + }, + "RATE_LIMIT_FREE_SYNC": { + "type": "string", + "description": "Free tier sync API executions per minute" + }, + "RATE_LIMIT_PRO_SYNC": { + "type": "string", + "description": "Pro tier sync API executions per minute" + }, + "RATE_LIMIT_TEAM_SYNC": { + "type": "string", + "description": "Team tier sync API executions per minute" + }, + "RATE_LIMIT_ENTERPRISE_SYNC": { + "type": "string", + "description": "Enterprise tier sync API executions per minute" + }, + "RATE_LIMIT_FREE_ASYNC": { + "type": "string", + "description": "Free tier async API executions per minute" + }, + "RATE_LIMIT_PRO_ASYNC": { + "type": "string", + "description": "Pro tier async API executions per minute" + }, + "RATE_LIMIT_TEAM_ASYNC": { + "type": "string", + "description": "Team tier async API executions per minute" + }, + "RATE_LIMIT_ENTERPRISE_ASYNC": { + "type": "string", + "description": "Enterprise tier async API executions per minute" + }, + "MANUAL_EXECUTION_LIMIT": { + "type": "string", + "description": "Manual execution bypass value" + }, + "NEXT_PUBLIC_BRAND_NAME": { + "type": "string", + "description": "Custom brand name" + }, + "NEXT_PUBLIC_BRAND_LOGO_URL": { + "type": "string", + "description": "Custom logo URL" + }, + "NEXT_PUBLIC_BRAND_FAVICON_URL": { + "type": "string", + "description": "Custom favicon URL" + }, + "NEXT_PUBLIC_BRAND_PRIMARY_COLOR": { + "type": "string", + "description": "Primary brand color (hex)" + }, + "NEXT_PUBLIC_BRAND_SECONDARY_COLOR": { + "type": "string", + "description": "Secondary brand color (hex)" + }, + "NEXT_PUBLIC_BRAND_ACCENT_COLOR": { + "type": "string", + "description": "Accent brand color (hex)" + }, + "NEXT_PUBLIC_CUSTOM_CSS_URL": { + "type": "string", + "description": "Custom stylesheet URL" + }, + "NEXT_PUBLIC_HIDE_BRANDING": { + "type": "string", + "description": "Hide powered by branding" + }, + "NEXT_PUBLIC_CUSTOM_FOOTER_TEXT": { + "type": "string", + "description": "Custom footer text" + }, + "NEXT_PUBLIC_SUPPORT_EMAIL": { + "type": "string", + "format": "email", + "description": "Support email address" + }, + "NEXT_PUBLIC_SUPPORT_URL": { + "type": "string", + "description": "Support page URL" + }, + "NEXT_PUBLIC_DOCUMENTATION_URL": { + "type": "string", + "description": "Documentation URL" + }, + "NEXT_PUBLIC_TERMS_URL": { + "type": "string", + "description": "Terms of service URL" + }, + "NEXT_PUBLIC_PRIVACY_URL": { + "type": "string", + "description": "Privacy policy URL" + }, + "NEXT_PUBLIC_SENTRY_DSN": { + "type": "string", + "description": "Sentry DSN for error tracking" + }, + "ALLOWED_LOGIN_EMAILS": { + "type": "string", + "description": "Comma-separated list of allowed email addresses for login" + }, + "ALLOWED_LOGIN_DOMAINS": { + "type": "string", + "description": "Comma-separated list of allowed email domains for login" } } } @@ -192,6 +375,11 @@ "ALLOWED_ORIGINS": { "type": "string", "description": "CORS allowed origins" + }, + "NODE_ENV": { + "type": "string", + "enum": ["development", "test", "production"], + "description": "Runtime environment" } } } diff --git a/helm/sim/values.yaml b/helm/sim/values.yaml index 201f4f39d4..f6f79ddb4e 100644 --- a/helm/sim/values.yaml +++ b/helm/sim/values.yaml @@ -64,13 +64,39 @@ app: BETTER_AUTH_SECRET: "" # REQUIRED - set via --set flag or external secret manager ENCRYPTION_KEY: "" # REQUIRED - set via --set flag or external secret manager - # Optional third-party service integrations (leave empty if not using) - FREESTYLE_API_KEY: "" - GOOGLE_CLIENT_ID: "" - GOOGLE_CLIENT_SECRET: "" - GITHUB_CLIENT_ID: "" - GITHUB_CLIENT_SECRET: "" - RESEND_API_KEY: "" + # Email & Communication + RESEND_API_KEY: "" # Resend API key for transactional emails + EMAIL_DOMAIN: "" # Domain for sending emails + + # OAuth Integration Credentials (leave empty if not using) + GOOGLE_CLIENT_ID: "" # Google OAuth client ID + GOOGLE_CLIENT_SECRET: "" # Google OAuth client secret + GITHUB_CLIENT_ID: "" # GitHub OAuth client ID + GITHUB_CLIENT_SECRET: "" # GitHub OAuth client secret + + # AI Provider API Keys (leave empty if not using) + OPENAI_API_KEY: "" # Primary OpenAI API key + OPENAI_API_KEY_1: "" # Additional OpenAI API key for load balancing + OPENAI_API_KEY_2: "" # Additional OpenAI API key for load balancing + OPENAI_API_KEY_3: "" # Additional OpenAI API key for load balancing + MISTRAL_API_KEY: "" # Mistral AI API key + ANTHROPIC_API_KEY_1: "" # Primary Anthropic Claude API key + ANTHROPIC_API_KEY_2: "" # Additional Anthropic API key for load balancing + ANTHROPIC_API_KEY_3: "" # Additional Anthropic API key for load balancing + OLLAMA_URL: "" # Ollama local LLM server URL + ELEVENLABS_API_KEY: "" # ElevenLabs API key for text-to-speech in deployed chat + + # Rate Limiting Configuration (per minute) + RATE_LIMIT_WINDOW_MS: "60000" # Rate limit window duration (1 minute) + RATE_LIMIT_FREE_SYNC: "10" # Free tier sync API executions + RATE_LIMIT_PRO_SYNC: "25" # Pro tier sync API executions + RATE_LIMIT_TEAM_SYNC: "75" # Team tier sync API executions + RATE_LIMIT_ENTERPRISE_SYNC: "150" # Enterprise tier sync API executions + RATE_LIMIT_FREE_ASYNC: "50" # Free tier async API executions + RATE_LIMIT_PRO_ASYNC: "200" # Pro tier async API executions + RATE_LIMIT_TEAM_ASYNC: "500" # Team tier async API executions + RATE_LIMIT_ENTERPRISE_ASYNC: "1000" # Enterprise tier async API executions + MANUAL_EXECUTION_LIMIT: "999999" # Manual execution bypass value # UI Branding & Whitelabeling Configuration NEXT_PUBLIC_BRAND_NAME: "Sim" # Custom brand name @@ -88,17 +114,13 @@ app: NEXT_PUBLIC_TERMS_URL: "" # Terms of service URL (leave empty for none) NEXT_PUBLIC_PRIVACY_URL: "" # Privacy policy URL (leave empty for none) - # Rate Limiting Configuration - RATE_LIMIT_WINDOW_MS: "60000" # Rate limit window in milliseconds (1 minute) - MANUAL_EXECUTION_LIMIT: "999999" # Manual execution limit (effectively unlimited) - RATE_LIMIT_FREE_SYNC: "10" # Free tier sync API executions per minute - RATE_LIMIT_FREE_ASYNC: "50" # Free tier async API executions per minute - RATE_LIMIT_PRO_SYNC: "25" # Pro tier sync API executions per minute - RATE_LIMIT_PRO_ASYNC: "200" # Pro tier async API executions per minute - RATE_LIMIT_TEAM_SYNC: "75" # Team tier sync API executions per minute - RATE_LIMIT_TEAM_ASYNC: "500" # Team tier async API executions per minute - RATE_LIMIT_ENTERPRISE_SYNC: "150" # Enterprise tier sync API executions per minute - RATE_LIMIT_ENTERPRISE_ASYNC: "1000" # Enterprise tier async API executions per minute + # Monitoring & Analytics (leave empty if not using) + NEXT_PUBLIC_SENTRY_DSN: "" # Sentry DSN for error tracking + + # Access Control (leave empty if not restricting login) + ALLOWED_LOGIN_EMAILS: "" # Comma-separated list of allowed email addresses for login + ALLOWED_LOGIN_DOMAINS: "" # Comma-separated list of allowed email domains for login + # Service configuration service: diff --git a/scripts/generate-block-docs.ts b/scripts/generate-block-docs.ts old mode 100644 new mode 100755 index 55c9efbd44..f1a9501972 --- a/scripts/generate-block-docs.ts +++ b/scripts/generate-block-docs.ts @@ -29,22 +29,9 @@ interface BlockConfig { longDescription?: string category: string bgColor?: string - icon?: any - subBlocks?: Array<{ - id: string - title?: string - placeholder?: string - type?: string - layout?: string - required?: boolean - options?: Array<{ label: string; id: string }> - [key: string]: any - }> - inputs?: Record outputs?: Record tools?: { access?: string[] - config?: any } [key: string]: any } @@ -111,8 +98,7 @@ function extractIcons(): Record { // Function to extract block configuration from file content function extractBlockConfig(fileContent: string): BlockConfig | null { try { - // Match the block name and type from imports and export statement - const _typeMatch = fileContent.match(/type\s+(\w+)Response\s*=/) + // Extract the block name from export statement const exportMatch = fileContent.match(/export\s+const\s+(\w+)Block\s*:/) if (!exportMatch) { @@ -243,8 +229,6 @@ function extractIconName(content: string): string | null { return iconMatch ? iconMatch[1] : null } -// Helper to extract subBlocks array - // Updated function to extract outputs with a simpler and more reliable approach function extractOutputs(content: string): Record { // Look for the outputs section using balanced brace matching @@ -348,112 +332,6 @@ function extractOutputs(content: string): Record { return outputs } } - - // Fallback: Try to extract fields from the old nested format - const fieldMatches = outputsContent.match(/(\w+)\s*:\s*{([^}]+)}/g) - - if (fieldMatches && fieldMatches.length > 0) { - fieldMatches.forEach((fieldMatch) => { - const fieldNameMatch = fieldMatch.match(/(\w+)\s*:/) - if (fieldNameMatch) { - const fieldName = fieldNameMatch[1] - - // Check if there's a type with a nested structure - const typeMatch = fieldMatch.match(/type\s*:\s*{([^}]+)}/) - if (typeMatch) { - // Handle nested type object - const typeContent = typeMatch[1] - const properties: Record = {} - - // Extract property types from the type object - handle cases with comments - // const propertyMatches = typeContent.match(/(\w+)\s*:\s*['"]([^'"]+)['"]/g) - const propertyMatches = typeContent.match( - /(\w+)\s*:\s*['"](.*?)['"](?:\s*,)?(?:\s*\/\/[^\n]*)?/g - ) - if (propertyMatches) { - propertyMatches.forEach((propMatch) => { - // Extract the property name and type, ignoring any trailing comments - const propParts = propMatch.match(/(\w+)\s*:\s*['"](.*?)['"]/) - if (propParts) { - const propName = propParts[1] - const propType = propParts[2] - - // Look for an inline comment that might contain a description - const commentMatch = propMatch.match(/\/\/\s*(.+)$/) - const description = commentMatch - ? commentMatch[1].trim() - : `${propName} of the ${fieldName}` - - properties[propName] = { - type: propType, - description: description, - } - } - }) - } - - // Add the field with properties - outputs[fieldName] = { - properties, - description: `${fieldName} from the block execution`, - } - } else { - // Try to extract a simple type definition - const simpleTypeMatch = fieldMatch.match(/type\s*:\s*['"]([^'"]+)['"]/) - if (simpleTypeMatch) { - outputs[fieldName] = { - type: simpleTypeMatch[1], - description: `${fieldName} output from the block`, - } - } - } - } - }) - } - - // If we parsed anything, return it - if (Object.keys(outputs).length > 0) { - return outputs - } - } - - // Fallback to the original method for backward compatibility - const outputsSection = content.match(/outputs\s*:\s*{([^}]*response[^}]*)}(?:\s*,|\s*})/s) - - if (outputsSection) { - // Find the response type definition - const responseTypeMatch = content.match(/response\s*:\s*{\s*type\s*:\s*{([^}]*)}/s) - - if (responseTypeMatch) { - const typeContent = responseTypeMatch[1] - - // Extract all field: 'type' pairs regardless of comments or formatting - const fieldMatches = typeContent.match(/(\w+)\s*:\s*['"](.*?)['"]/g) - - if (fieldMatches && fieldMatches.length > 0) { - const typeFields: Record = {} - - // Process each field match - fieldMatches.forEach((match) => { - const fieldParts = match.match(/(\w+)\s*:\s*['"](.*?)['"]/) - if (fieldParts) { - const fieldName = fieldParts[1] - const fieldType = fieldParts[2] - typeFields[fieldName] = fieldType - } - }) - - // If we have any fields, return them in the expected structure - if (Object.keys(typeFields).length > 0) { - const result = { - response: { - type: typeFields, - }, - } - return result - } - } - } } return {} @@ -483,16 +361,16 @@ function extractToolsAccess(content: string): string[] { // Function to extract tool information from file content function extractToolInfo( toolName: string, - fileContent: string, - filePath = '' + fileContent: string ): { description: string params: Array<{ name: string; type: string; required: boolean; description: string }> outputs: Record } | null { try { - // Extract tool config section - Simplified regex to match any *Tool export pattern - const toolConfigRegex = /export const \w+Tool\s*[=<][^{]*{[\s\S]*?params\s*:\s*{([\s\S]*?)}/im + // Extract tool config section - Match params until the next top-level property + const toolConfigRegex = + /params\s*:\s*{([\s\S]*?)},?\s*(?:outputs|oauth|request|directExecution|postProcess|transformResponse|transformError)/ const toolConfigMatch = fileContent.match(toolConfigRegex) // Extract description @@ -506,14 +384,38 @@ function extractToolInfo( if (toolConfigMatch) { const paramsContent = toolConfigMatch[1] - // More robust approach to extract parameters + // More robust approach to extract parameters with balanced brace matching // Extract each parameter block completely - const paramBlocksRegex = /(\w+)\s*:\s*{([^}]+)}/g + const paramBlocksRegex = /(\w+)\s*:\s*{/g let paramMatch + const paramPositions: Array<{ name: string; start: number; content: string }> = [] while ((paramMatch = paramBlocksRegex.exec(paramsContent)) !== null) { const paramName = paramMatch[1] - const paramBlock = paramMatch[2] + const startPos = paramMatch.index + paramMatch[0].length - 1 // Position of opening brace + + // Find matching closing brace using balanced counting + let braceCount = 1 + let endPos = startPos + 1 + + while (endPos < paramsContent.length && braceCount > 0) { + if (paramsContent[endPos] === '{') { + braceCount++ + } else if (paramsContent[endPos] === '}') { + braceCount-- + } + endPos++ + } + + if (braceCount === 0) { + const paramBlock = paramsContent.substring(startPos + 1, endPos - 1).trim() + paramPositions.push({ name: paramName, start: startPos, content: paramBlock }) + } + } + + for (const param of paramPositions) { + const paramName = param.name + const paramBlock = param.content // Skip the accessToken parameter as it's handled automatically by the OAuth flow // Also skip any params parameter which isn't a real input @@ -526,13 +428,19 @@ function extractToolInfo( const requiredMatch = paramBlock.match(/required\s*:\s*(true|false)/) // More careful extraction of description with handling for multiline descriptions - let descriptionMatch = paramBlock.match(/description\s*:\s*'(.*?)'/) + let descriptionMatch = paramBlock.match(/description\s*:\s*'(.*?)'(?=\s*[,}])/s) if (!descriptionMatch) { - descriptionMatch = paramBlock.match(/description\s*:\s*"(.*?)"/) + descriptionMatch = paramBlock.match(/description\s*:\s*"(.*?)"(?=\s*[,}])/s) } if (!descriptionMatch) { // Try for template literals if the description uses backticks - descriptionMatch = paramBlock.match(/description\s*:\s*`([^`]+)`/) + descriptionMatch = paramBlock.match(/description\s*:\s*`([^`]+)`/s) + } + if (!descriptionMatch) { + // Handle multi-line descriptions without ending quote on same line + descriptionMatch = paramBlock.match( + /description\s*:\s*['"]([^'"]*(?:\n[^'"]*)*?)['"](?=\s*[,}])/s + ) } params.push({ @@ -544,203 +452,341 @@ function extractToolInfo( } } - // If no params were found with the first method, try a more direct regex approach - if (params.length === 0) { - const paramRegex = - /(\w+)\s*:\s*{(?:[^{}]|{[^{}]*})*type\s*:\s*['"](.*?)['"](?:[^{}]|{[^{}]*})*required\s*:\s*(true|false)(?:[^{}]|{[^{}]*})*description\s*:\s*['"](.*?)['"](?:[^{}]|{[^{}]*})*}/g - let match + // First priority: Extract outputs from the new outputs field in ToolConfig + let outputs: Record = {} + const outputsFieldRegex = + /outputs\s*:\s*{([\s\S]*?)}\s*,?\s*(?:oauth|params|request|directExecution|postProcess|transformResponse|transformError|$|\})/ + const outputsFieldMatch = fileContent.match(outputsFieldRegex) + + if (outputsFieldMatch) { + const outputsContent = outputsFieldMatch[1] + outputs = parseToolOutputsField(outputsContent) + console.log(`Found tool outputs field for ${toolName}:`, Object.keys(outputs)) + } - while ((match = paramRegex.exec(fileContent)) !== null) { - // Skip the accessToken parameter and any params parameter - if (match[1] === 'params' || match[1] === 'tools') continue + return { + description, + params, + outputs, + } + } catch (error) { + console.error(`Error extracting info for tool ${toolName}:`, error) + return null + } +} - params.push({ - name: match[1], - type: match[2], - required: match[3] === 'true', - description: match[4] || 'No description', - }) +// Helper function to recursively format output structure for documentation +function formatOutputStructure(outputs: Record, indentLevel = 0): string { + let result = '' + + for (const [key, output] of Object.entries(outputs)) { + let type = 'unknown' + let description = `${key} output from the tool` + + if (typeof output === 'object' && output !== null) { + if (output.type) { + type = output.type + } + + if (output.description) { + description = output.description } } - // Extract output structure from transformResponse - let outputs: Record = {} - const outputRegex = /transformResponse[\s\S]*?return\s*{[\s\S]*?output\s*:\s*{([^}]*)/ - const outputMatch = fileContent.match(outputRegex) + // Escape special characters in the description + const escapedDescription = description + .replace(/\|/g, '\\|') + .replace(/\{/g, '\\{') + .replace(/\}/g, '\\}') + .replace(/\(/g, '\\(') + .replace(/\)/g, '\\)') + .replace(/\[/g, '\\[') + .replace(/\]/g, '\\]') + .replace(//g, '>') - if (outputMatch) { - const outputContent = outputMatch[1] - // Try to parse the output structure based on the content - outputs = parseOutputStructure(toolName, outputContent) + // Create prefix based on nesting level with visual hierarchy + let prefix = '' + if (indentLevel === 1) { + prefix = '↳ ' + } else if (indentLevel >= 2) { + // For deeper nesting (like array items), use indented arrows + prefix = ' ↳ ' } - // If we couldn't extract outputs from transformResponse, try an alternative approach - if (Object.keys(outputs).length === 0) { - // Look for output in successful response in transformResponse - const successOutputRegex = - /success\s*:\s*true,\s*output\s*:\s*(\{[^}]*\}|\w+(\.\w+)+\s*\|\|\s*\{[^}]*\}|\w+(\.\w+)+\.map\s*\()/ - const successOutputMatch = fileContent.match(successOutputRegex) + // For arrays, expand nested items + if (typeof output === 'object' && output !== null && output.type === 'array') { + result += `| ${prefix}\`${key}\` | ${type} | ${escapedDescription} |\n` - if (successOutputMatch) { - const outputExpression = successOutputMatch[1].trim() - - // Handle case where output is something like "data.data || {}" - if (outputExpression.includes('||')) { - outputs.data = 'json' - } - // Handle array mapping like "data.issues.map(...)" - else if (outputExpression.includes('.map')) { - // Try to extract the array object being mapped - const arrayMapMatch = outputExpression.match(/(\w+(?:\.\w+)+)\.map/) - if (arrayMapMatch) { - const arrayPath = arrayMapMatch[1] - // Get the base object being mapped to an array - const arrayObject = arrayPath.split('.').pop() - if (arrayObject) { - outputs[arrayObject] = 'Array of mapped items' - } - } else { - // Fallback if we can't extract the exact array object - outputs.items = 'Array of mapped items' - } - } - // Handle direct object assignment like "output: { field1, field2 }" - else if (outputExpression.startsWith('{')) { - const fieldMatches = outputExpression.match(/(\w+)\s*:/g) - if (fieldMatches) { - fieldMatches.forEach((match) => { - const fieldName = match.trim().replace(':', '') - outputs[fieldName] = 'Dynamic output field' - }) - } - } - // Check for data.X patterns like "data.data" - else if (outputExpression.includes('.')) { - const fieldName = outputExpression.split('.').pop() - if (fieldName) { - outputs[fieldName] = 'json' - } - } + // Handle array items with properties (nested TWO more levels to show it's inside the array) + if (output.items?.properties) { + // Create a visual separator to show these are array item properties + const arrayItemsResult = formatOutputStructure(output.items.properties, indentLevel + 2) + result += arrayItemsResult } } + // For objects, expand properties + else if ( + typeof output === 'object' && + output !== null && + output.properties && + (output.type === 'object' || output.type === 'json') + ) { + result += `| ${prefix}\`${key}\` | ${type} | ${escapedDescription} |\n` - // Try to extract TypeScript interface for outputs as a fallback - if (Object.keys(outputs).length === 0) { - const interfaceRegex = new RegExp( - `interface\\s+${toolName.replace(/_/g, '')}Response\\s*{[\\s\\S]*?output\\s*:\\s*{([\\s\\S]*?)}[\\s\\S]*?}` - ) - const interfaceMatch = fileContent.match(interfaceRegex) + const nestedResult = formatOutputStructure(output.properties, indentLevel + 1) + result += nestedResult + } + // For simple types, show with prefix if nested + else { + result += `| ${prefix}\`${key}\` | ${type} | ${escapedDescription} |\n` + } + } - if (interfaceMatch) { - const interfaceContent = interfaceMatch[1] - outputs = parseOutputStructure(toolName, interfaceContent) - } + return result +} + +// New function to parse the structured outputs field from ToolConfig +function parseToolOutputsField(outputsContent: string): Record { + const outputs: Record = {} + + // Calculate nesting levels for all braces first + const braces: Array<{ type: 'open' | 'close'; pos: number; level: number }> = [] + for (let i = 0; i < outputsContent.length; i++) { + if (outputsContent[i] === '{') { + braces.push({ type: 'open', pos: i, level: 0 }) + } else if (outputsContent[i] === '}') { + braces.push({ type: 'close', pos: i, level: 0 }) } + } - // Look for TypeScript types in a types.ts file if available - if (Object.keys(outputs).length === 0 && filePath) { - const toolDir = path.dirname(filePath) - const typesPath = path.join(toolDir, 'types.ts') - if (fs.existsSync(typesPath)) { - const typesContent = fs.readFileSync(typesPath, 'utf-8') - const responseTypeRegex = new RegExp( - `interface\\s+${toolName.replace(/_/g, '')}Response\\s*extends\\s+\\w+\\s*{\\s*output\\s*:\\s*{([\\s\\S]*?)}\\s*}`, - 'i' - ) - const responseTypeMatch = typesContent.match(responseTypeRegex) + // Calculate actual nesting levels + let currentLevel = 0 + for (const brace of braces) { + if (brace.type === 'open') { + brace.level = currentLevel + currentLevel++ + } else { + currentLevel-- + brace.level = currentLevel + } + } + + // Find field definitions and their nesting levels + const fieldStartRegex = /(\w+)\s*:\s*{/g + let match + const fieldPositions: Array<{ name: string; start: number; end: number; level: number }> = [] - if (responseTypeMatch) { - outputs = parseOutputStructure(toolName, responseTypeMatch[1]) + while ((match = fieldStartRegex.exec(outputsContent)) !== null) { + const fieldName = match[1] + const bracePos = match.index + match[0].length - 1 + + // Find the corresponding opening brace to determine nesting level + const openBrace = braces.find((b) => b.type === 'open' && b.pos === bracePos) + if (openBrace) { + // Find the matching closing brace + let braceCount = 1 + let endPos = bracePos + 1 + + while (endPos < outputsContent.length && braceCount > 0) { + if (outputsContent[endPos] === '{') { + braceCount++ + } else if (outputsContent[endPos] === '}') { + braceCount-- } + endPos++ } - } - return { - description, - params, - outputs, + fieldPositions.push({ + name: fieldName, + start: bracePos, + end: endPos, + level: openBrace.level, + }) } - } catch (error) { - console.error(`Error extracting info for tool ${toolName}:`, error) - return null } + + // Only process level 0 fields (top-level outputs) + const topLevelFields = fieldPositions.filter((f) => f.level === 0) + + topLevelFields.forEach((field) => { + const fieldContent = outputsContent.substring(field.start + 1, field.end - 1).trim() + + // Parse the field content + const parsedField = parseFieldContent(fieldContent) + if (parsedField) { + outputs[field.name] = parsedField + } + }) + + return outputs } -// Update the parseOutputStructure function to better handle nested objects -function parseOutputStructure(toolName: string, outputContent: string): Record { - const outputs: Record = {} +// Helper function to parse individual field content with support for nested structures +function parseFieldContent(fieldContent: string): any { + // Extract type and description + const typeMatch = fieldContent.match(/type\s*:\s*['"]([^'"]+)['"]/) + const descMatch = fieldContent.match(/description\s*:\s*['"`]([^'"`\n]+)['"`]/) + + if (!typeMatch) return null + + const fieldType = typeMatch[1] + const description = descMatch ? descMatch[1] : '' - // Try to extract field declarations with their types - const fieldRegex = /(\w+)\s*:([^,}]+)/g - let fieldMatch + const result: any = { + type: fieldType, + description: description, + } + + // Check for properties (nested objects) - only for object types, not arrays + if (fieldType === 'object' || fieldType === 'json') { + const propertiesRegex = /properties\s*:\s*{/ + const propertiesStart = fieldContent.search(propertiesRegex) - while ((fieldMatch = fieldRegex.exec(outputContent)) !== null) { - const fieldName = fieldMatch[1].trim() + if (propertiesStart !== -1) { + const braceStart = fieldContent.indexOf('{', propertiesStart) + let braceCount = 1 + let braceEnd = braceStart + 1 - // Determine a good description based on field name - let description = 'Dynamic output field' + // Find matching closing brace + while (braceEnd < fieldContent.length && braceCount > 0) { + if (fieldContent[braceEnd] === '{') braceCount++ + else if (fieldContent[braceEnd] === '}') braceCount-- + braceEnd++ + } - if (fieldName === 'results' || fieldName === 'memories' || fieldName === 'searchResults') { - description = `${fieldName.charAt(0).toUpperCase() + fieldName.slice(1)} from the operation` - } else if (fieldName === 'ids') { - description = 'IDs of created or retrieved resources' - } else if (fieldName === 'answer') { - description = 'Generated answer text' - } else if (fieldName === 'citations') { - description = 'References used to generate the answer' + if (braceCount === 0) { + const propertiesContent = fieldContent.substring(braceStart + 1, braceEnd - 1).trim() + result.properties = parsePropertiesContent(propertiesContent) + } } + } + + // Check for items (array items) - ensure balanced brace matching + const itemsRegex = /items\s*:\s*{/ + const itemsStart = fieldContent.search(itemsRegex) + + if (itemsStart !== -1) { + const braceStart = fieldContent.indexOf('{', itemsStart) + let braceCount = 1 + let braceEnd = braceStart + 1 + + // Find matching closing brace + while (braceEnd < fieldContent.length && braceCount > 0) { + if (fieldContent[braceEnd] === '{') braceCount++ + else if (fieldContent[braceEnd] === '}') braceCount-- + braceEnd++ + } + + if (braceCount === 0) { + const itemsContent = fieldContent.substring(braceStart + 1, braceEnd - 1).trim() + const itemsType = itemsContent.match(/type\s*:\s*['"]([^'"]+)['"]/) + + // Only look for description before any properties block to avoid picking up nested property descriptions + const propertiesStart = itemsContent.search(/properties\s*:\s*{/) + const searchContent = + propertiesStart >= 0 ? itemsContent.substring(0, propertiesStart) : itemsContent + const itemsDesc = searchContent.match(/description\s*:\s*['"`]([^'"`\n]+)['"`]/) + + result.items = { + type: itemsType ? itemsType[1] : 'object', + description: itemsDesc ? itemsDesc[1] : '', + } + + // Check if items have properties + const itemsPropertiesRegex = /properties\s*:\s*{/ + const itemsPropsStart = itemsContent.search(itemsPropertiesRegex) + + if (itemsPropsStart !== -1) { + const propsBraceStart = itemsContent.indexOf('{', itemsPropsStart) + let propsBraceCount = 1 + let propsBraceEnd = propsBraceStart + 1 - outputs[fieldName] = description + while (propsBraceEnd < itemsContent.length && propsBraceCount > 0) { + if (itemsContent[propsBraceEnd] === '{') propsBraceCount++ + else if (itemsContent[propsBraceEnd] === '}') propsBraceCount-- + propsBraceEnd++ + } + + if (propsBraceCount === 0) { + const itemsPropsContent = itemsContent + .substring(propsBraceStart + 1, propsBraceEnd - 1) + .trim() + result.items.properties = parsePropertiesContent(itemsPropsContent) + } + } + } } - const shorthandRegex = /(?:^\s*|[,{]\s*)([A-Za-z_][\w]*)\s*(?=,|})/g - let shorthandMatch + return result +} - while ((shorthandMatch = shorthandRegex.exec(outputContent)) !== null) { - const fieldName = shorthandMatch[1].trim() +// Helper function to parse properties content recursively +function parsePropertiesContent(propertiesContent: string): Record { + const properties: Record = {} - // Ignore fields already captured or those that are part of key/value pairs - if (outputs[fieldName]) continue + // Find property definitions using balanced brace matching, but exclude type-only definitions + const propStartRegex = /(\w+)\s*:\s*{/g + let match + const propPositions: Array<{ name: string; start: number; content: string }> = [] - // Provide the same heuristic descriptions as above - let description = 'Dynamic output field' + while ((match = propStartRegex.exec(propertiesContent)) !== null) { + const propName = match[1] - if (fieldName === 'results' || fieldName === 'memories' || fieldName === 'searchResults') { - description = `${fieldName.charAt(0).toUpperCase() + fieldName.slice(1)} from the operation` - } else if (fieldName === 'ids') { - description = 'IDs of created or retrieved resources' - } else if (fieldName === 'answer') { - description = 'Generated answer text' - } else if (fieldName === 'citations') { - description = 'References used to generate the answer' + // Skip structural keywords that should never be treated as property names + if (propName === 'items' || propName === 'properties') { + continue } - outputs[fieldName] = description - } + const startPos = match.index + match[0].length - 1 // Position of opening brace - // Try to identify common patterns based on tool types - if (Object.keys(outputs).length === 0) { - if (toolName.includes('_search')) { - outputs.results = 'Array of search results' - } else if (toolName.includes('_answer')) { - outputs.answer = 'Generated answer text' - outputs.citations = 'References used to generate the answer' - } else if (toolName.includes('_add')) { - outputs.ids = 'IDs of created resources' - } else if (toolName.includes('_get')) { - outputs.data = 'Retrieved data' - } else { - // Try to extract field names from the output content with a simpler regex - const simpleFieldsRegex = /(\w+)\s*:/g - let simpleFieldMatch + // Find the matching closing brace + let braceCount = 1 + let endPos = startPos + 1 - while ((simpleFieldMatch = simpleFieldsRegex.exec(outputContent)) !== null) { - outputs[simpleFieldMatch[1]] = 'Dynamic output field' + while (endPos < propertiesContent.length && braceCount > 0) { + if (propertiesContent[endPos] === '{') { + braceCount++ + } else if (propertiesContent[endPos] === '}') { + braceCount-- + } + endPos++ + } + + if (braceCount === 0) { + const propContent = propertiesContent.substring(startPos + 1, endPos - 1).trim() + + // Skip if this is just a type definition (contains only 'type' field) rather than a real property + // This happens with array items definitions like: items: { type: 'string' } + // More precise check: only skip if it ONLY has 'type' and nothing else meaningful + const hasDescription = /description\s*:\s*/.test(propContent) + const hasProperties = /properties\s*:\s*{/.test(propContent) + const hasItems = /items\s*:\s*{/.test(propContent) + const isTypeOnly = + !hasDescription && + !hasProperties && + !hasItems && + /^type\s*:\s*['"].*?['"]\s*,?\s*$/.test(propContent) + + if (!isTypeOnly) { + propPositions.push({ + name: propName, + start: startPos, + content: propContent, + }) } } } - return outputs + // Process the actual property definitions + propPositions.forEach((prop) => { + const parsedProp = parseFieldContent(prop.content) + if (parsedProp) { + properties[prop.name] = parsedProp + } + }) + + return properties } // Find and extract information about a tool @@ -784,14 +830,6 @@ async function getToolInfo(toolName: string): Promise<{ // Most common pattern: suffix.ts file in the prefix directory possibleLocations.push(path.join(rootDir, `apps/sim/tools/${toolPrefix}/${toolSuffix}.ts`)) - // Try underscore version if suffix has multiple parts - if (toolSuffix.includes('_')) { - const underscoreSuffix = toolSuffix.replace(/_/g, '_') - possibleLocations.push( - path.join(rootDir, `apps/sim/tools/${toolPrefix}/${underscoreSuffix}.ts`) - ) - } - // Try camelCase version of suffix const camelCaseSuffix = toolSuffix .split('_') @@ -803,45 +841,22 @@ async function getToolInfo(toolName: string): Promise<{ possibleLocations.push(path.join(rootDir, `apps/sim/tools/${toolPrefix}/index.ts`)) // Try to find the tool definition file - let toolFilePath = '' let toolFileContent = '' for (const location of possibleLocations) { if (fs.existsSync(location)) { - toolFilePath = location toolFileContent = fs.readFileSync(location, 'utf-8') break } } - // If not found, search in tool-specific directory - if (!toolFileContent) { - const toolsDir = path.join(rootDir, 'apps/tools') - if (fs.existsSync(path.join(toolsDir, toolPrefix))) { - const dirPath = path.join(toolsDir, toolPrefix) - const files = fs.readdirSync(dirPath).filter((file) => file.endsWith('.ts')) - - for (const file of files) { - const filePath = path.join(dirPath, file) - const content = fs.readFileSync(filePath, 'utf-8') - - // Check if this file contains the tool id - if (content.includes(`id: '${toolName}'`) || content.includes(`id: "${toolName}"`)) { - toolFilePath = filePath - toolFileContent = content - break - } - } - } - } - if (!toolFileContent) { console.warn(`Could not find definition for tool: ${toolName}`) return null } // Extract tool information from the file - return extractToolInfo(toolName, toolFileContent, toolFilePath) + return extractToolInfo(toolName, toolFileContent) } catch (error) { console.error(`Error getting info for tool ${toolName}:`, error) return null @@ -897,9 +912,6 @@ function mergeWithManualContent( usage: { regex: /## Usage Instructions/, }, - configuration: { - regex: /## Configuration/, - }, outputs: { regex: /## Outputs/, }, @@ -1011,7 +1023,7 @@ async function generateMarkdownForBlock( bgColor, iconName, outputs = {}, - tools = { access: [], config: {} }, + tools = { access: [] }, } = blockConfig // Get SVG icon if available @@ -1138,48 +1150,47 @@ async function generateMarkdownForBlock( // Add Output Parameters section for the tool toolsSection += '\n#### Output\n\n' - // Prefer block outputs over tool outputs if available, since block outputs have better descriptions - const outputsToUse = Object.keys(outputs).length > 0 ? outputs : toolInfo.outputs - - if (Object.keys(outputsToUse).length > 0) { - // Use block outputs if available, otherwise tool outputs - if (Object.keys(outputs).length > 0) { - // Generate table with block outputs (which have descriptions) - toolsSection += '| Parameter | Type | Description |\n' - toolsSection += '| --------- | ---- | ----------- |\n' - - for (const [key, output] of Object.entries(outputs)) { - let type = 'string' - let description = `${key} output from the tool` - - if (typeof output === 'string') { - type = output - } else if (typeof output === 'object' && output !== null) { - if ('type' in output && typeof output.type === 'string') { - type = output.type - } - if ('description' in output && typeof output.description === 'string') { - description = output.description - } + // Always prefer tool-specific outputs over block outputs for accuracy + if (Object.keys(toolInfo.outputs).length > 0) { + // Use tool-specific outputs (most accurate) + toolsSection += '| Parameter | Type | Description |\n' + toolsSection += '| --------- | ---- | ----------- |\n' + + // Use the enhanced formatOutputStructure function to handle nested structures + toolsSection += formatOutputStructure(toolInfo.outputs) + } else if (Object.keys(outputs).length > 0) { + // Fallback to block outputs only if no tool outputs are available + toolsSection += '| Parameter | Type | Description |\n' + toolsSection += '| --------- | ---- | ----------- |\n' + + for (const [key, output] of Object.entries(outputs)) { + let type = 'string' + let description = `${key} output from the tool` + + if (typeof output === 'string') { + type = output + } else if (typeof output === 'object' && output !== null) { + if ('type' in output && typeof output.type === 'string') { + type = output.type + } + if ('description' in output && typeof output.description === 'string') { + description = output.description } - - // Escape special characters in the description - const escapedDescription = description - .replace(/\|/g, '\\|') - .replace(/\{/g, '\\{') - .replace(/\}/g, '\\}') - .replace(/\(/g, '\\(') - .replace(/\)/g, '\\)') - .replace(/\[/g, '\\[') - .replace(/\]/g, '\\]') - .replace(//g, '>') - - toolsSection += `| \`${key}\` | ${type} | ${escapedDescription} |\n` } - } else { - // Use dynamically extracted tool outputs as fallback - toolsSection += generateMarkdownTable(toolInfo.outputs) + + // Escape special characters in the description + const escapedDescription = description + .replace(/\|/g, '\\|') + .replace(/\{/g, '\\{') + .replace(/\}/g, '\\}') + .replace(/\(/g, '\\(') + .replace(/\)/g, '\\)') + .replace(/\[/g, '\\[') + .replace(/\]/g, '\\]') + .replace(//g, '>') + + toolsSection += `| \`${key}\` | ${type} | ${escapedDescription} |\n` } } else { toolsSection += 'This tool does not produce any outputs.\n' @@ -1286,34 +1297,3 @@ generateAllBlockDocs() console.error('Fatal error:', error) process.exit(1) }) - -function generateMarkdownTable(outputs: Record): string { - let table = '' - table += '| Parameter | Type | Description |\n' - table += '| --------- | ---- | ----------- |\n' - - for (const [key, value] of Object.entries(outputs)) { - // Try to determine a reasonable type from the value description - let inferredType = 'string' - if (value.toLowerCase().includes('array')) inferredType = 'array' - if (value.toLowerCase().includes('json')) inferredType = 'json' - if (value.toLowerCase().includes('number')) inferredType = 'number' - if (value.toLowerCase().includes('boolean')) inferredType = 'boolean' - - // Escape special characters in the description - const escapedDescription = value - .replace(/\|/g, '\\|') - .replace(/\{/g, '\\{') - .replace(/\}/g, '\\}') - .replace(/\(/g, '\\(') - .replace(/\)/g, '\\)') - .replace(/\[/g, '\\[') - .replace(/\]/g, '\\]') - .replace(//g, '>') - - table += `| \`${key}\` | ${inferredType} | ${escapedDescription} |\n` - } - - return table -}