From d651fce70fc2a8d2431c69e69a4c18353ca232b0 Mon Sep 17 00:00:00 2001 From: waleed Date: Tue, 9 Jun 2026 20:11:30 -0700 Subject: [PATCH 1/7] improvement(tools): validate integrations, add Gong activity tools, regenerate docs --- apps/docs/components/icons.tsx | 4 +- .../docs/content/docs/en/tools/brightdata.mdx | 2 +- .../docs/content/docs/en/tools/databricks.mdx | 116 +++++ apps/docs/content/docs/en/tools/dub.mdx | 135 +++++- .../docs/content/docs/en/tools/duckduckgo.mdx | 4 + apps/docs/content/docs/en/tools/enrich.mdx | 102 ++++ apps/docs/content/docs/en/tools/fireflies.mdx | 7 + apps/docs/content/docs/en/tools/gong.mdx | 100 +++- apps/docs/content/docs/en/tools/intercom.mdx | 2 +- apps/docs/content/docs/en/tools/mailchimp.mdx | 2 +- .../content/docs/en/tools/millionverifier.mdx | 2 +- .../docs/content/docs/en/tools/servicenow.mdx | 101 ++++ .../docs/content/docs/en/tools/zerobounce.mdx | 2 +- .../content/docs/en/triggers/intercom.mdx | 2 +- .../servicenow/upload-attachment/route.ts | 121 +++++ .../app/api/tools/workday/change-job/route.ts | 20 +- .../api/tools/workday/update-worker/route.ts | 2 +- apps/sim/blocks/blocks/brightdata.ts | 2 +- apps/sim/blocks/blocks/databricks.ts | 58 ++- apps/sim/blocks/blocks/dub.ts | 451 ++++++++++++++++-- apps/sim/blocks/blocks/duckduckgo.ts | 9 + apps/sim/blocks/blocks/enrich.ts | 125 ++++- apps/sim/blocks/blocks/fireflies.ts | 32 ++ apps/sim/blocks/blocks/gong.ts | 223 ++++++++- apps/sim/blocks/blocks/google_pagespeed.ts | 47 +- apps/sim/blocks/blocks/intercom.ts | 2 +- apps/sim/blocks/blocks/mailchimp.ts | 2 +- apps/sim/blocks/blocks/millionverifier.ts | 2 +- apps/sim/blocks/blocks/okta.ts | 9 +- apps/sim/blocks/blocks/servicenow.ts | 202 +++++++- apps/sim/blocks/blocks/workday.ts | 5 +- apps/sim/blocks/blocks/zerobounce.ts | 2 +- apps/sim/components/icons.tsx | 4 +- apps/sim/lib/api/contracts/tools/index.ts | 1 + .../sim/lib/api/contracts/tools/servicenow.ts | 26 + apps/sim/lib/integrations/integrations.json | 100 +++- apps/sim/tools/databricks/cancel_run.ts | 5 +- apps/sim/tools/databricks/execute_sql.ts | 5 +- apps/sim/tools/databricks/get_cluster.ts | 137 ++++++ apps/sim/tools/databricks/get_job.ts | 105 ++++ apps/sim/tools/databricks/get_run.ts | 5 +- apps/sim/tools/databricks/get_run_output.ts | 5 +- apps/sim/tools/databricks/get_statement.ts | 136 ++++++ apps/sim/tools/databricks/index.ts | 8 + apps/sim/tools/databricks/list_clusters.ts | 5 +- apps/sim/tools/databricks/list_jobs.ts | 5 +- apps/sim/tools/databricks/list_runs.ts | 5 +- apps/sim/tools/databricks/list_warehouses.ts | 127 +++++ apps/sim/tools/databricks/run_job.ts | 5 +- apps/sim/tools/databricks/types.ts | 89 +++- apps/sim/tools/dub/bulk_create_links.ts | 73 +++ apps/sim/tools/dub/bulk_delete_links.ts | 62 +++ apps/sim/tools/dub/bulk_update_links.ts | 85 ++++ apps/sim/tools/dub/get_events.ts | 142 ++++++ apps/sim/tools/dub/get_links_count.ts | 119 +++++ apps/sim/tools/dub/get_qr_code.ts | 122 +++++ apps/sim/tools/dub/index.ts | 12 + apps/sim/tools/dub/list_links.ts | 14 - apps/sim/tools/dub/types.ts | 104 +++- apps/sim/tools/duckduckgo/search.ts | 34 +- apps/sim/tools/duckduckgo/types.ts | 12 +- apps/sim/tools/enrich/index.ts | 6 + .../enrich/linkedin_to_personal_email.ts | 19 +- apps/sim/tools/enrich/search_jobs.ts | 149 ++++++ .../enrich/search_post_comments_by_url.ts | 143 ++++++ .../enrich/search_post_reactions_by_url.ts | 121 +++++ apps/sim/tools/enrich/types.ts | 40 ++ apps/sim/tools/fireflies/create_bite.ts | 2 +- apps/sim/tools/fireflies/delete_transcript.ts | 42 +- apps/sim/tools/fireflies/types.ts | 8 + apps/sim/tools/gong/aggregate_by_period.ts | 219 +++++++++ apps/sim/tools/gong/day_by_day_activity.ts | 208 ++++++++ apps/sim/tools/gong/get_extensive_calls.ts | 46 +- apps/sim/tools/gong/index.ts | 4 + apps/sim/tools/gong/types.ts | 87 ++++ apps/sim/tools/okta/activate_user.ts | 2 +- apps/sim/tools/okta/add_user_to_group.ts | 2 +- apps/sim/tools/okta/deactivate_user.ts | 2 +- apps/sim/tools/okta/delete_group.ts | 2 +- apps/sim/tools/okta/delete_user.ts | 2 +- apps/sim/tools/okta/get_group.ts | 2 +- apps/sim/tools/okta/get_user.ts | 2 +- apps/sim/tools/okta/list_group_members.ts | 2 +- apps/sim/tools/okta/remove_user_from_group.ts | 2 +- apps/sim/tools/okta/reset_password.ts | 2 +- apps/sim/tools/okta/suspend_user.ts | 2 +- apps/sim/tools/okta/unsuspend_user.ts | 2 +- apps/sim/tools/okta/update_group.ts | 2 +- apps/sim/tools/okta/update_user.ts | 2 +- apps/sim/tools/registry.ts | 38 ++ apps/sim/tools/servicenow/aggregate.ts | 199 ++++++++ apps/sim/tools/servicenow/create_record.ts | 4 +- apps/sim/tools/servicenow/delete_record.ts | 4 +- .../tools/servicenow/download_attachment.ts | 115 +++++ apps/sim/tools/servicenow/index.ts | 8 + apps/sim/tools/servicenow/list_attachments.ts | 135 ++++++ apps/sim/tools/servicenow/read_record.ts | 6 +- apps/sim/tools/servicenow/types.ts | 95 ++++ apps/sim/tools/servicenow/update_record.ts | 4 +- .../sim/tools/servicenow/upload_attachment.ts | 112 +++++ apps/sim/tools/workday/types.ts | 15 +- 101 files changed, 5120 insertions(+), 184 deletions(-) create mode 100644 apps/sim/app/api/tools/servicenow/upload-attachment/route.ts create mode 100644 apps/sim/lib/api/contracts/tools/servicenow.ts create mode 100644 apps/sim/tools/databricks/get_cluster.ts create mode 100644 apps/sim/tools/databricks/get_job.ts create mode 100644 apps/sim/tools/databricks/get_statement.ts create mode 100644 apps/sim/tools/databricks/list_warehouses.ts create mode 100644 apps/sim/tools/dub/bulk_create_links.ts create mode 100644 apps/sim/tools/dub/bulk_delete_links.ts create mode 100644 apps/sim/tools/dub/bulk_update_links.ts create mode 100644 apps/sim/tools/dub/get_events.ts create mode 100644 apps/sim/tools/dub/get_links_count.ts create mode 100644 apps/sim/tools/dub/get_qr_code.ts create mode 100644 apps/sim/tools/enrich/search_jobs.ts create mode 100644 apps/sim/tools/enrich/search_post_comments_by_url.ts create mode 100644 apps/sim/tools/enrich/search_post_reactions_by_url.ts create mode 100644 apps/sim/tools/gong/aggregate_by_period.ts create mode 100644 apps/sim/tools/gong/day_by_day_activity.ts create mode 100644 apps/sim/tools/servicenow/aggregate.ts create mode 100644 apps/sim/tools/servicenow/download_attachment.ts create mode 100644 apps/sim/tools/servicenow/list_attachments.ts create mode 100644 apps/sim/tools/servicenow/upload_attachment.ts diff --git a/apps/docs/components/icons.tsx b/apps/docs/components/icons.tsx index dc6f5ea5094..41732af3b29 100644 --- a/apps/docs/components/icons.tsx +++ b/apps/docs/components/icons.tsx @@ -4987,7 +4987,7 @@ export function IntercomIcon(props: SVGProps) { @@ -5028,7 +5028,7 @@ export function MailchimpIcon(props: SVGProps) { y='0px' viewBox='0 0 230.81 244.96' xmlSpace='preserve' - fill='currentColor' + fill='#000000' > diff --git a/apps/docs/content/docs/en/tools/brightdata.mdx b/apps/docs/content/docs/en/tools/brightdata.mdx index f3d1772779b..65f8327e862 100644 --- a/apps/docs/content/docs/en/tools/brightdata.mdx +++ b/apps/docs/content/docs/en/tools/brightdata.mdx @@ -7,7 +7,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card" ## Usage Instructions diff --git a/apps/docs/content/docs/en/tools/databricks.mdx b/apps/docs/content/docs/en/tools/databricks.mdx index a1ded4fab96..8b85a82fd12 100644 --- a/apps/docs/content/docs/en/tools/databricks.mdx +++ b/apps/docs/content/docs/en/tools/databricks.mdx @@ -64,6 +64,61 @@ Execute a SQL statement against a Databricks SQL warehouse and return results in | `totalRows` | number | Total number of rows in the result | | `truncated` | boolean | Whether the result set was truncated due to row_limit or byte_limit | +### `databricks_get_statement` + +Poll a SQL statement by its ID to retrieve status and results. Use this after Execute SQL when a query runs longer than the wait timeout. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `host` | string | Yes | Databricks workspace host \(e.g., dbc-abc123.cloud.databricks.com\) | +| `apiKey` | string | Yes | Databricks Personal Access Token | +| `statementId` | string | Yes | The ID of the statement to fetch \(returned by Execute SQL\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `statementId` | string | Unique identifier for the statement | +| `status` | string | Execution status \(SUCCEEDED, PENDING, RUNNING, FAILED, CANCELED, CLOSED\) | +| `columns` | array | Column schema of the result set | +| ↳ `name` | string | Column name | +| ↳ `position` | number | Column position \(0-based\) | +| ↳ `typeName` | string | Column type \(STRING, INT, LONG, DOUBLE, BOOLEAN, TIMESTAMP, DATE, DECIMAL, etc.\) | +| `data` | array | Result rows as a 2D array of strings where each inner array is a row of column values | +| `totalRows` | number | Total number of rows in the result | +| `truncated` | boolean | Whether the result set was truncated due to row_limit or byte_limit | + +### `databricks_list_warehouses` + +List all SQL warehouses in a Databricks workspace including their size, state, and type. Use this to discover the warehouse ID needed for Execute SQL. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `host` | string | Yes | Databricks workspace host \(e.g., dbc-abc123.cloud.databricks.com\) | +| `apiKey` | string | Yes | Databricks Personal Access Token | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `warehouses` | array | List of SQL warehouses in the workspace | +| ↳ `warehouseId` | string | Unique warehouse identifier | +| ↳ `name` | string | Warehouse display name | +| ↳ `clusterSize` | string | Warehouse size \(e.g., 2X-Small, Small, Medium, Large\) | +| ↳ `state` | string | Current state \(STARTING, RUNNING, STOPPING, STOPPED, DELETING, DELETED\) | +| ↳ `warehouseType` | string | Warehouse type \(CLASSIC, PRO\) | +| ↳ `creatorName` | string | Email of the warehouse creator | +| ↳ `autoStopMinutes` | number | Minutes of inactivity before auto-stop \(0 = disabled\) | +| ↳ `numClusters` | number | Current number of running clusters | +| ↳ `minNumClusters` | number | Minimum cluster count for scaling | +| ↳ `maxNumClusters` | number | Maximum cluster count for scaling | +| ↳ `numActiveSessions` | number | Number of active sessions | +| ↳ `enableServerlessCompute` | boolean | Whether serverless compute is enabled | + ### `databricks_list_jobs` List all jobs in a Databricks workspace with optional filtering by name. @@ -93,6 +148,34 @@ List all jobs in a Databricks workspace with optional filtering by name. | `hasMore` | boolean | Whether more jobs are available for pagination | | `nextPageToken` | string | Token for fetching the next page of results | +### `databricks_get_job` + +Get the full definition and settings of a single Databricks job by its job ID. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `host` | string | Yes | Databricks workspace host \(e.g., dbc-abc123.cloud.databricks.com\) | +| `apiKey` | string | Yes | Databricks Personal Access Token | +| `jobId` | number | Yes | The canonical identifier of the job to retrieve | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `jobId` | number | The job ID | +| `name` | string | Job name | +| `creatorUserName` | string | Email of the job creator | +| `runAsUserName` | string | User the job runs as | +| `createdTime` | number | Job creation timestamp \(epoch ms\) | +| `format` | string | Job format \(SINGLE_TASK or MULTI_TASK\) | +| `maxConcurrentRuns` | number | Maximum number of concurrent runs | +| `timeoutSeconds` | number | Job-level timeout in seconds \(0 or null means no timeout\) | +| `schedule` | object | Cron schedule configuration \(quartz_cron_expression, timezone_id, pause_status\) | +| `tags` | object | Key-value tags applied to the job | +| `tasks` | array | Task definitions for the job \(empty for single-task jobs\) | + ### `databricks_run_job` Trigger an existing Databricks job to run immediately with optional job-level or notebook parameters. @@ -264,4 +347,37 @@ List all clusters in a Databricks workspace including their state, configuration | ↳ `autoterminationMinutes` | number | Minutes of inactivity before auto-termination \(0 = disabled\) | | ↳ `startTime` | number | Cluster start timestamp \(epoch ms\) | +### `databricks_get_cluster` + +Get the state, configuration, and resource details of a single Databricks cluster by its cluster ID. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `host` | string | Yes | Databricks workspace host \(e.g., dbc-abc123.cloud.databricks.com\) | +| `apiKey` | string | Yes | Databricks Personal Access Token | +| `clusterId` | string | Yes | The ID of the cluster to retrieve | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `cluster` | object | Cluster detail | +| ↳ `clusterId` | string | Unique cluster identifier | +| ↳ `clusterName` | string | Cluster display name | +| ↳ `state` | string | Current state \(PENDING, RUNNING, RESTARTING, RESIZING, TERMINATING, TERMINATED, ERROR, UNKNOWN\) | +| ↳ `stateMessage` | string | Human-readable state description | +| ↳ `creatorUserName` | string | Email of the cluster creator | +| ↳ `sparkVersion` | string | Spark runtime version \(e.g., 13.3.x-scala2.12\) | +| ↳ `nodeTypeId` | string | Worker node type identifier | +| ↳ `driverNodeTypeId` | string | Driver node type identifier | +| ↳ `numWorkers` | number | Number of worker nodes \(for fixed-size clusters\) | +| ↳ `autoscale` | object | Autoscaling configuration \(null for fixed-size clusters\) | +| ↳ `minWorkers` | number | Minimum number of workers | +| ↳ `maxWorkers` | number | Maximum number of workers | +| ↳ `clusterSource` | string | Origin \(API, UI, JOB, MODELS, PIPELINE, PIPELINE_MAINTENANCE, SQL\) | +| ↳ `autoterminationMinutes` | number | Minutes of inactivity before auto-termination \(0 = disabled\) | +| ↳ `startTime` | number | Cluster start timestamp \(epoch ms\) | + diff --git a/apps/docs/content/docs/en/tools/dub.mdx b/apps/docs/content/docs/en/tools/dub.mdx index fe8cada02ef..feedbdec7fd 100644 --- a/apps/docs/content/docs/en/tools/dub.mdx +++ b/apps/docs/content/docs/en/tools/dub.mdx @@ -273,8 +273,6 @@ Retrieve a paginated list of short links for the authenticated workspace. Suppor | `search` | string | No | Search query matched against the short link slug and destination URL | | `tagIds` | string | No | Comma-separated tag IDs to filter by | | `showArchived` | boolean | No | Whether to include archived links \(defaults to false\) | -| `sortBy` | string | No | Sort by field: createdAt, clicks, saleAmount, or lastClicked | -| `sortOrder` | string | No | Sort order: asc or desc | | `page` | number | No | Page number \(default: 1\) | | `pageSize` | number | No | Number of links per page \(default: 100, max: 100\) | @@ -285,6 +283,86 @@ Retrieve a paginated list of short links for the authenticated workspace. Suppor | `links` | json | Array of link objects \(id, domain, key, url, shortLink, clicks, tags, createdAt\) | | `count` | number | Number of links returned | +### `dub_get_links_count` + +Retrieve the number of short links for the authenticated workspace, optionally filtered and grouped by domain, tag, user, or folder. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Dub API key | +| `domain` | string | No | Filter by domain | +| `search` | string | No | Search query matched against the short link slug and destination URL | +| `tagIds` | string | No | Comma-separated tag IDs to filter by | +| `tagNames` | string | No | Comma-separated tag names to filter by \(case-insensitive\) | +| `folderId` | string | No | Filter by folder ID | +| `showArchived` | boolean | No | Whether to include archived links \(defaults to false\) | +| `groupBy` | string | No | Group counts by: domain, tagId, userId, or folderId | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `count` | number | Total number of links matching the filters | +| `groups` | json | Per-group counts when groupBy is set \(e.g. \[\{ domain, count \}\]\) | + +### `dub_bulk_create_links` + +Create up to 100 short links in a single request. Returns the created links alongside any per-link errors. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Dub API key | +| `links` | json | Yes | JSON array of link objects to create. Each object requires a "url" and may include domain, key, tagIds, and other link fields \(max 100\). | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `created` | json | Array of successfully created link objects | +| `errors` | json | Array of per-link errors \(\{ link, error, code \}\) for links that failed | +| `count` | number | Number of links successfully created | + +### `dub_bulk_update_links` + +Apply the same set of field updates to up to 100 links at once, selected by link IDs or external IDs. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Dub API key | +| `linkIds` | string | No | Comma-separated link IDs to update \(max 100, takes precedence over externalIds\) | +| `externalIds` | string | No | Comma-separated external IDs to update \(max 100\) | +| `data` | json | Yes | JSON object of fields to apply to every selected link \(e.g. \{ "archived": true, "tagIds": \["..."\] \}\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `updated` | json | Array of updated link objects | +| `count` | number | Number of links updated | + +### `dub_bulk_delete_links` + +Delete up to 100 short links in a single request by their link IDs. Non-existing IDs are ignored. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Dub API key | +| `linkIds` | string | Yes | Comma-separated link IDs to delete \(max 100\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `deletedCount` | number | Number of links that were deleted | + ### `dub_get_analytics` Retrieve analytics for links including clicks, leads, and sales. Supports filtering by link, time range, and grouping by various dimensions. @@ -315,4 +393,57 @@ Retrieve analytics for links including clicks, leads, and sales. Supports filter | `saleAmount` | number | Total sale amount in cents | | `data` | json | Grouped analytics data \(timeseries, countries, devices, etc.\) | +### `dub_get_events` + +Retrieve a paginated stream of individual click, lead, and sale events for links, with filtering by link, time range, and location. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Dub API key | +| `event` | string | No | Event type: clicks \(default\), leads, or sales | +| `linkId` | string | No | Filter by link ID | +| `externalId` | string | No | Filter by external ID \(prefix with ext_\) | +| `domain` | string | No | Filter by domain | +| `interval` | string | No | Time interval: 24h \(default\), 7d, 30d, 90d, 1y, mtd, qtd, ytd, or all | +| `start` | string | No | Start date/time in ISO 8601 format \(overrides interval\) | +| `end` | string | No | End date/time in ISO 8601 format \(defaults to now\) | +| `country` | string | No | Filter by country \(ISO 3166-1 alpha-2 code\) | +| `timezone` | string | No | IANA timezone for event timestamps \(defaults to UTC\) | +| `page` | number | No | Page number \(default: 1\) | +| `limit` | number | No | Number of events per page \(default: 100, max: 1000\) | +| `sortOrder` | string | No | Sort order: desc \(default\) or asc | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `events` | json | Array of event objects \(event, timestamp, click, link, and customer/sale data when applicable\) | +| `count` | number | Number of events returned | + +### `dub_get_qr_code` + +Generate a customizable QR code (PNG) for a short link, with control over size, error correction, colors, and margin. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Dub API key | +| `url` | string | Yes | The short link URL to encode in the QR code | +| `size` | number | No | QR code size in pixels \(default: 600\) | +| `level` | string | No | Error correction level: L \(default\), M, Q, or H | +| `fgColor` | string | No | Foreground color in hex \(default: #000000\) | +| `bgColor` | string | No | Background color in hex \(default: #FFFFFF\) | +| `hideLogo` | boolean | No | Whether to hide the logo in the center of the QR code \(default: false\) | +| `margin` | number | No | Margin \(quiet zone\) around the QR code \(default: 2\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `file` | file | Generated QR code image stored in execution files | +| `content` | string | Base64-encoded PNG image data | + diff --git a/apps/docs/content/docs/en/tools/duckduckgo.mdx b/apps/docs/content/docs/en/tools/duckduckgo.mdx index 5cf62617e26..babd175c10f 100644 --- a/apps/docs/content/docs/en/tools/duckduckgo.mdx +++ b/apps/docs/content/docs/en/tools/duckduckgo.mdx @@ -54,10 +54,14 @@ Search the web using DuckDuckGo Instant Answers API. Returns instant answers, ab | `abstractText` | string | Plain text version of the abstract | | `abstractSource` | string | The source of the abstract \(e.g., Wikipedia\) | | `abstractURL` | string | URL to the source of the abstract | +| `definition` | string | Dictionary-style definition if available | +| `definitionSource` | string | The source of the definition | +| `definitionURL` | string | URL to the source of the definition | | `image` | string | URL to an image related to the topic | | `answer` | string | Direct answer if available \(e.g., for calculations\) | | `answerType` | string | Type of the answer \(e.g., calc, ip, etc.\) | | `type` | string | Response type: A \(article\), D \(disambiguation\), C \(category\), N \(name\), E \(exclusive\) | +| `redirect` | string | !bang redirect URL, populated only for bang queries | | `relatedTopics` | array | Array of related topics with URLs and descriptions | | ↳ `FirstURL` | string | URL to the related topic | | ↳ `Text` | string | Description of the related topic | diff --git a/apps/docs/content/docs/en/tools/enrich.mdx b/apps/docs/content/docs/en/tools/enrich.mdx index 5e03c3e6fb8..c2850ef25c6 100644 --- a/apps/docs/content/docs/en/tools/enrich.mdx +++ b/apps/docs/content/docs/en/tools/enrich.mdx @@ -690,6 +690,41 @@ Advanced people search with complex filters for location, company size, seniorit | ↳ `start` | number | Start position | | ↳ `limit` | number | Limit | +### `enrich_search_jobs` + +Search LinkedIn job postings by keywords with filters for location, job type, workplace type, experience level, and company. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Enrich API key | +| `keywords` | string | Yes | Search keywords \(e.g., "software engineer"\) | +| `location` | string | No | Location filter \(e.g., London\) | +| `jobTypes` | string | No | Comma-separated job types \(e.g., "full time, part time"\) | +| `workplaceTypes` | string | No | Comma-separated workplace types \(e.g., "on site, remote"\) | +| `experienceLevels` | string | No | Comma-separated experience levels \(e.g., "internship, associate"\) | +| `companyIds` | string | No | Comma-separated LinkedIn company IDs to filter by \(e.g., "2048, 3050"\) | +| `timePosted` | string | No | Time filter \(e.g., past_24hrs, past_week, past_month\) | +| `start` | number | No | Number of records to skip for pagination \(default: 0\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `count` | number | Number of job postings returned | +| `jobs` | array | Job postings | +| ↳ `title` | string | Job title | +| ↳ `companyName` | string | Hiring company name | +| ↳ `companyLink` | string | Company LinkedIn URL | +| ↳ `companyLogo` | string | Company logo URL | +| ↳ `location` | string | Job location | +| ↳ `url` | string | Job posting URL | +| ↳ `postedDate` | string | Date the job was posted | +| ↳ `postedTimestamp` | string | Timestamp the job was posted | +| ↳ `hiringStatus` | string | Hiring status | +| ↳ `criteria` | object | Employment criteria \(seniority, type, function\) | + ### `enrich_search_posts` Search LinkedIn posts by keywords with date filtering. @@ -780,6 +815,35 @@ Get reactions on a LinkedIn post with filtering by reaction type. | ↳ `profilePicture` | string | Profile picture URL | | ↳ `linkedInUrl` | string | LinkedIn URL | +### `enrich_search_post_reactions_by_url` + +Get reactions on a LinkedIn post by its URL, filtered by reaction type. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Enrich API key | +| `postUrl` | string | Yes | LinkedIn post URL \(e.g., https://www.linkedin.com/posts/...\) | +| `reactionType` | string | Yes | Reaction type filter: all, like, love, celebrate, insightful, or funny \(default: all\) | +| `page` | number | Yes | Page number \(starts at 1\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `page` | number | Current page number | +| `totalPage` | number | Total number of pages | +| `count` | number | Number of reactions returned | +| `reactions` | array | Reactions | +| ↳ `reactionType` | string | Type of reaction | +| ↳ `reactor` | object | Person who reacted | +| ↳ `name` | string | Name | +| ↳ `subTitle` | string | Job title | +| ↳ `profileId` | string | Profile ID | +| ↳ `profilePicture` | string | Profile picture URL | +| ↳ `linkedInUrl` | string | LinkedIn URL | + ### `enrich_search_post_comments` Get comments on a LinkedIn post. @@ -818,6 +882,44 @@ Get comments on a LinkedIn post. | ↳ `empathy` | number | Number of empathy reactions | | ↳ `other` | number | Number of other reactions | +### `enrich_search_post_comments_by_url` + +Get comments on a LinkedIn post by its URL. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Enrich API key | +| `postUrl` | string | Yes | LinkedIn post URL \(e.g., https://www.linkedin.com/posts/...\) | +| `page` | number | No | Page number \(starts at 1, default: 1\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `page` | number | Current page number | +| `totalPage` | number | Total number of pages | +| `count` | number | Number of comments returned | +| `comments` | array | Comments | +| ↳ `activityId` | string | Comment activity ID | +| ↳ `commentary` | string | Comment text | +| ↳ `linkedInUrl` | string | Link to comment | +| ↳ `commenter` | object | Commenter info | +| ↳ `profileId` | string | Profile ID | +| ↳ `firstName` | string | First name | +| ↳ `lastName` | string | Last name | +| ↳ `subTitle` | string | Subtitle/headline | +| ↳ `profilePicture` | string | Profile picture URL | +| ↳ `backgroundImage` | string | Background image URL | +| ↳ `entityUrn` | string | Entity URN | +| ↳ `objectUrn` | string | Object URN | +| ↳ `profileType` | string | Profile type | +| ↳ `reactionBreakdown` | object | Reactions on the comment | +| ↳ `likes` | number | Number of likes | +| ↳ `empathy` | number | Number of empathy reactions | +| ↳ `other` | number | Number of other reactions | + ### `enrich_search_people_activities` Get a person diff --git a/apps/docs/content/docs/en/tools/fireflies.mdx b/apps/docs/content/docs/en/tools/fireflies.mdx index 90c59160525..5205acb4d97 100644 --- a/apps/docs/content/docs/en/tools/fireflies.mdx +++ b/apps/docs/content/docs/en/tools/fireflies.mdx @@ -171,6 +171,13 @@ Delete a transcript from Fireflies.ai | Parameter | Type | Description | | --------- | ---- | ----------- | | `success` | boolean | Whether the transcript was successfully deleted | +| `transcript` | object | The deleted transcript | +| ↳ `id` | string | Transcript ID | +| ↳ `title` | string | Meeting title | +| ↳ `date` | number | Meeting timestamp | +| ↳ `duration` | number | Meeting duration | +| ↳ `host_email` | string | Host email address | +| ↳ `organizer_email` | string | Organizer email address | ### `fireflies_add_to_live_meeting` diff --git a/apps/docs/content/docs/en/tools/gong.mdx b/apps/docs/content/docs/en/tools/gong.mdx index f01b1ae395a..2234d2c27a9 100644 --- a/apps/docs/content/docs/en/tools/gong.mdx +++ b/apps/docs/content/docs/en/tools/gong.mdx @@ -184,7 +184,7 @@ Retrieve transcripts of calls from Gong by call IDs or date range. ### `gong_get_extensive_calls` -Retrieve detailed call data including trackers, topics, and highlights from Gong. +Retrieve detailed call data including trackers, topics, highlights, and AI spotlight content (brief, outline, key points, call outcome) from Gong. #### Input @@ -240,6 +240,17 @@ Retrieve detailed call data including trackers, topics, and highlights from Gong | ↳ `methods` | array | Whether invited or attended | | ↳ `context` | array | Links to external systems for this party | | ↳ `content` | object | Call content data | +| ↳ `brief` | string | AI-generated brief summary of the call \(Call Spotlight\) | +| ↳ `outline` | array | AI-generated call outline sections | +| ↳ `section` | string | Outline section name | +| ↳ `startTime` | number | Section start in seconds from call start | +| ↳ `duration` | number | Section duration in seconds | +| ↳ `keyPoints` | array | AI-generated key points of the call | +| ↳ `text` | string | Key point text | +| ↳ `callOutcome` | object | AI-determined call outcome \(Call Spotlight\) | +| ↳ `id` | string | Outcome category ID | +| ↳ `category` | string | Outcome category name | +| ↳ `name` | string | Outcome name | | ↳ `structure` | array | Call agenda parts | | ↳ `name` | string | Agenda name | | ↳ `duration` | number | Duration of this part in seconds | @@ -425,6 +436,93 @@ Retrieve aggregated activity statistics for users by date range from Gong. | `toDateTime` | string | End of results in ISO-8601 format | | `cursor` | string | Pagination cursor for the next page | +### `gong_day_by_day_activity` + +Retrieve detailed day-by-day activity (call IDs per activity type) for users by date range from Gong. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `accessKey` | string | Yes | Gong API Access Key | +| `accessKeySecret` | string | Yes | Gong API Access Key Secret | +| `userIds` | string | No | Comma-separated list of Gong user IDs \(up to 20 digits each\) | +| `fromDate` | string | Yes | Start date in YYYY-MM-DD format \(inclusive, in company timezone\) | +| `toDate` | string | Yes | End date in YYYY-MM-DD format \(exclusive, in company timezone, cannot exceed current day\) | +| `cursor` | string | No | Pagination cursor from a previous response | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `requestId` | string | A Gong request reference ID for troubleshooting purposes | +| `usersDetailedActivities` | array | Day-by-day activity per user, with call IDs grouped by activity type | +| ↳ `userId` | string | Gong's unique numeric identifier for the user | +| ↳ `userEmailAddress` | string | Email address of the Gong user | +| ↳ `userDailyActivityStats` | array | One record per day in the date range | +| ↳ `fromDate` | string | Start of the day \(ISO-8601\) | +| ↳ `toDate` | string | End of the day \(ISO-8601\) | +| ↳ `callsAsHost` | array | IDs of calls the user hosted | +| ↳ `callsAttended` | array | IDs of calls the user attended \(not host\) | +| ↳ `callsGaveFeedback` | array | IDs of calls the user gave feedback on | +| ↳ `callsReceivedFeedback` | array | IDs of calls the user received feedback on | +| ↳ `callsRequestedFeedback` | array | IDs of calls the user requested feedback on | +| ↳ `callsScorecardsFilled` | array | IDs of calls the user filled scorecards on | +| ↳ `callsScorecardsReceived` | array | IDs of the user's calls that received a scorecard | +| ↳ `ownCallsListenedTo` | array | IDs of the user's own calls the user listened to | +| ↳ `othersCallsListenedTo` | array | IDs of other users' calls the user listened to | +| ↳ `callsSharedInternally` | array | IDs of calls the user shared internally | +| ↳ `callsSharedExternally` | array | IDs of calls the user shared externally | +| ↳ `callsCommentsGiven` | array | IDs of calls the user commented on | +| ↳ `callsCommentsReceived` | array | IDs of the user's calls that received a comment | +| ↳ `callsMarkedAsFeedbackGiven` | array | IDs of calls the user marked as reviewed | +| ↳ `callsMarkedAsFeedbackReceived` | array | IDs of the user's calls marked as reviewed by others | +| `cursor` | string | Pagination cursor for the next page | + +### `gong_aggregate_by_period` + +Retrieve aggregated user activity grouped into time periods (day, week, month, quarter, year) by date range from Gong. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `accessKey` | string | Yes | Gong API Access Key | +| `accessKeySecret` | string | Yes | Gong API Access Key Secret | +| `aggregationPeriod` | string | Yes | Calendar period to group activity by: DAY, WEEK, MONTH, QUARTER, or YEAR \(week starts Monday\) | +| `userIds` | string | No | Comma-separated list of Gong user IDs \(up to 20 digits each\) | +| `fromDate` | string | Yes | Start date in YYYY-MM-DD format \(inclusive, in company timezone\) | +| `toDate` | string | Yes | End date in YYYY-MM-DD format \(exclusive, in company timezone, cannot exceed current day\) | +| `cursor` | string | No | Pagination cursor from a previous response | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `requestId` | string | A Gong request reference ID for troubleshooting purposes | +| `usersAggregateActivity` | array | Aggregated activity per user, one item per consecutive time period in the range | +| ↳ `userId` | string | Gong's unique numeric identifier for the user | +| ↳ `userEmailAddress` | string | Email address of the Gong user | +| ↳ `userAggregateActivity` | array | Activity counts per time period | +| ↳ `fromDate` | string | Start of the period \(ISO-8601\) | +| ↳ `toDate` | string | End of the period \(ISO-8601\) | +| ↳ `callsAsHost` | number | Calls the user hosted | +| ↳ `callsAttended` | number | Calls the user attended \(not host\) | +| ↳ `callsGaveFeedback` | number | Calls the user gave feedback on | +| ↳ `callsReceivedFeedback` | number | Calls the user received feedback on | +| ↳ `callsRequestedFeedback` | number | Calls the user requested feedback on | +| ↳ `callsScorecardsFilled` | number | Scorecards the user completed | +| ↳ `callsScorecardsReceived` | number | Calls where someone filled a scorecard on the user's calls | +| ↳ `ownCallsListenedTo` | number | The user's own calls the user listened to | +| ↳ `othersCallsListenedTo` | number | Other users' calls the user listened to | +| ↳ `callsSharedInternally` | number | Calls the user shared internally | +| ↳ `callsSharedExternally` | number | Calls the user shared externally | +| ↳ `callsCommentsGiven` | number | Calls the user commented on | +| ↳ `callsCommentsReceived` | number | Calls where the user's calls received a comment | +| ↳ `callsMarkedAsFeedbackGiven` | number | Calls the user marked as reviewed | +| ↳ `callsMarkedAsFeedbackReceived` | number | The user's calls marked as reviewed by others | +| `cursor` | string | Pagination cursor for the next page | + ### `gong_interaction_stats` Retrieve interaction statistics for users by date range from Gong. Only includes calls with Whisper enabled. diff --git a/apps/docs/content/docs/en/tools/intercom.mdx b/apps/docs/content/docs/en/tools/intercom.mdx index 67a764e3bd8..2183765ba07 100644 --- a/apps/docs/content/docs/en/tools/intercom.mdx +++ b/apps/docs/content/docs/en/tools/intercom.mdx @@ -7,7 +7,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card" {/* MANUAL-CONTENT-START:intro */} diff --git a/apps/docs/content/docs/en/tools/mailchimp.mdx b/apps/docs/content/docs/en/tools/mailchimp.mdx index 305c2ceb380..35dc4033942 100644 --- a/apps/docs/content/docs/en/tools/mailchimp.mdx +++ b/apps/docs/content/docs/en/tools/mailchimp.mdx @@ -7,7 +7,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card" {/* MANUAL-CONTENT-START:intro */} diff --git a/apps/docs/content/docs/en/tools/millionverifier.mdx b/apps/docs/content/docs/en/tools/millionverifier.mdx index 2c24baeeab2..58e708ef292 100644 --- a/apps/docs/content/docs/en/tools/millionverifier.mdx +++ b/apps/docs/content/docs/en/tools/millionverifier.mdx @@ -7,7 +7,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card" {/* MANUAL-CONTENT-START:intro */} diff --git a/apps/docs/content/docs/en/tools/servicenow.mdx b/apps/docs/content/docs/en/tools/servicenow.mdx index 420fe6aee01..398e3fd6db2 100644 --- a/apps/docs/content/docs/en/tools/servicenow.mdx +++ b/apps/docs/content/docs/en/tools/servicenow.mdx @@ -123,4 +123,105 @@ Delete a record from a ServiceNow table | `success` | boolean | Whether the deletion was successful | | `metadata` | json | Operation metadata | +### `servicenow_aggregate` + +Compute aggregate statistics (count, sum, average, min, max, group by) over a ServiceNow table + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `instanceUrl` | string | Yes | ServiceNow instance URL \(e.g., https://instance.service-now.com\) | +| `username` | string | Yes | ServiceNow username | +| `password` | string | Yes | ServiceNow password | +| `tableName` | string | Yes | Table name \(e.g., incident, change_request, task\) | +| `query` | string | No | Encoded query string to filter records before aggregating \(e.g., "active=true"\) | +| `count` | boolean | No | Return the count of matching records | +| `groupBy` | string | No | Comma-separated fields to group results by \(e.g., category,priority\) | +| `avgFields` | string | No | Comma-separated numeric fields to average \(e.g., reassignment_count\) | +| `sumFields` | string | No | Comma-separated numeric fields to sum | +| `minFields` | string | No | Comma-separated fields to compute the minimum of | +| `maxFields` | string | No | Comma-separated fields to compute the maximum of | +| `having` | string | No | Filter on aggregate results \(e.g., "count>5"\) | +| `displayValue` | string | No | Return display values for grouped reference fields: "true", "false", or "all" | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `result` | json | Aggregate result. Ungrouped: \{stats: \{count, sum, avg, min, max\}\}. Grouped: array of \{stats, groupby_fields\}. | +| `count` | number | Total matching record count \(only present for ungrouped count queries\) | +| `metadata` | json | Operation metadata \(grouped, groupCount\) | + +### `servicenow_list_attachments` + +List the attachments on a ServiceNow record + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `instanceUrl` | string | Yes | ServiceNow instance URL \(e.g., https://instance.service-now.com\) | +| `username` | string | Yes | ServiceNow username | +| `password` | string | Yes | ServiceNow password | +| `tableName` | string | Yes | Table that owns the record \(e.g., incident, change_request\) | +| `recordSysId` | string | Yes | sys_id of the record whose attachments should be listed | +| `limit` | number | No | Maximum number of attachments to return | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `attachments` | array | Attachment metadata records | +| ↳ `sys_id` | string | Attachment sys_id | +| ↳ `file_name` | string | File name | +| ↳ `content_type` | string | MIME type | +| ↳ `size_bytes` | string | File size in bytes | +| ↳ `download_link` | string | Direct download URL for the file | +| `metadata` | json | Operation metadata \(recordCount\) | + +### `servicenow_download_attachment` + +Download an attachment file from ServiceNow by its sys_id + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `instanceUrl` | string | Yes | ServiceNow instance URL \(e.g., https://instance.service-now.com\) | +| `username` | string | Yes | ServiceNow username | +| `password` | string | Yes | ServiceNow password | +| `attachmentSysId` | string | Yes | sys_id of the attachment to download \(from List Attachments\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `file` | file | Downloaded attachment stored in execution files | +| `content` | string | Base64 encoded file content | + +### `servicenow_upload_attachment` + +Attach a file to a ServiceNow record + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `instanceUrl` | string | Yes | ServiceNow instance URL \(e.g., https://instance.service-now.com\) | +| `username` | string | Yes | ServiceNow username | +| `password` | string | Yes | ServiceNow password | +| `tableName` | string | Yes | Table that owns the record \(e.g., incident, change_request\) | +| `recordSysId` | string | Yes | sys_id of the record to attach the file to | +| `fileName` | string | Yes | Name to give the uploaded file \(e.g., logs.txt\) | +| `file` | file | No | File to upload \(UserFile object\) | +| `fileContent` | string | No | Base64-encoded file content \(legacy\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `attachment` | json | Created attachment metadata \(sys_id, file_name, content_type, download_link\) | +| `metadata` | json | Operation metadata | + diff --git a/apps/docs/content/docs/en/tools/zerobounce.mdx b/apps/docs/content/docs/en/tools/zerobounce.mdx index fd212b281de..257e7a5908f 100644 --- a/apps/docs/content/docs/en/tools/zerobounce.mdx +++ b/apps/docs/content/docs/en/tools/zerobounce.mdx @@ -7,7 +7,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card" {/* MANUAL-CONTENT-START:intro */} diff --git a/apps/docs/content/docs/en/triggers/intercom.mdx b/apps/docs/content/docs/en/triggers/intercom.mdx index 0930f009e18..6dd7b68f86e 100644 --- a/apps/docs/content/docs/en/triggers/intercom.mdx +++ b/apps/docs/content/docs/en/triggers/intercom.mdx @@ -7,7 +7,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card" Intercom provides 6 triggers for automating workflows based on events. diff --git a/apps/sim/app/api/tools/servicenow/upload-attachment/route.ts b/apps/sim/app/api/tools/servicenow/upload-attachment/route.ts new file mode 100644 index 00000000000..da0967dfcd9 --- /dev/null +++ b/apps/sim/app/api/tools/servicenow/upload-attachment/route.ts @@ -0,0 +1,121 @@ +import { createLogger } from '@sim/logger' +import { getErrorMessage } from '@sim/utils/errors' +import { type NextRequest, NextResponse } from 'next/server' +import { servicenowUploadAttachmentContract } from '@/lib/api/contracts/tools/servicenow' +import { parseRequest } from '@/lib/api/server' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { secureFetchWithValidation } from '@/lib/core/security/input-validation.server' +import { generateRequestId } from '@/lib/core/utils/request' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { processSingleFileToUserFile } from '@/lib/uploads/utils/file-utils' +import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server' +import { assertToolFileAccess } from '@/app/api/files/authorization' +import { createBasicAuthHeader } from '@/tools/servicenow/utils' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('ServiceNowUploadAttachmentAPI') + +export const POST = withRouteHandler(async (request: NextRequest) => { + const requestId = generateRequestId() + + try { + const authResult = await checkInternalAuth(request, { requireWorkflowId: false }) + + if (!authResult.success || !authResult.userId) { + logger.warn(`[${requestId}] Unauthorized ServiceNow upload attempt: ${authResult.error}`) + return NextResponse.json( + { success: false, error: authResult.error || 'Authentication required' }, + { status: 401 } + ) + } + + const parsed = await parseRequest(servicenowUploadAttachmentContract, request, {}) + if (!parsed.success) return parsed.response + const body = parsed.data.body + + let fileBuffer: Buffer + let contentType = 'application/octet-stream' + + if (body.file) { + let userFile + try { + userFile = processSingleFileToUserFile(body.file, requestId, logger) + } catch (error) { + return NextResponse.json( + { success: false, error: getErrorMessage(error, 'Failed to process file') }, + { status: 400 } + ) + } + + const denied = await assertToolFileAccess(userFile.key, authResult.userId, requestId, logger) + if (denied) return denied + + if (userFile.type) contentType = userFile.type + fileBuffer = await downloadFileFromStorage(userFile, requestId, logger) + } else if (body.fileContent) { + fileBuffer = Buffer.from(body.fileContent, 'base64') + } else { + return NextResponse.json( + { success: false, error: 'Either file or fileContent must be provided' }, + { status: 400 } + ) + } + + const baseUrl = body.instanceUrl.trim().replace(/\/$/, '') + const uploadParams = new URLSearchParams({ + table_name: body.tableName.trim(), + table_sys_id: body.recordSysId.trim(), + file_name: body.fileName, + }) + const uploadUrl = `${baseUrl}/api/now/attachment/file?${uploadParams.toString()}` + + const response = await secureFetchWithValidation( + uploadUrl, + { + method: 'POST', + headers: { + Authorization: createBasicAuthHeader(body.username, body.password), + 'Content-Type': contentType, + Accept: 'application/json', + }, + body: fileBuffer, + }, + 'instanceUrl' + ) + + if (!response.ok) { + const errorData = (await response.json().catch(() => ({}))) as { + error?: { message?: string } + } + const errorMessage = + errorData?.error?.message ?? + `ServiceNow API error: ${response.status} ${response.statusText}` + logger.error(`[${requestId}] ServiceNow upload attachment failed`, { + status: response.status, + }) + return NextResponse.json({ success: false, error: errorMessage }, { status: response.status }) + } + + const data = await response.json() + + logger.info(`[${requestId}] File attached to ServiceNow record successfully`, { + tableName: body.tableName, + recordSysId: body.recordSysId, + }) + + return NextResponse.json({ + success: true, + output: { + attachment: data.result, + metadata: { recordCount: 1 }, + }, + }) + } catch (error) { + logger.error(`[${requestId}] Error uploading attachment to ServiceNow:`, error) + return NextResponse.json( + { success: false, error: getErrorMessage(error, 'Internal server error') }, + { status: 500 } + ) + } +}) diff --git a/apps/sim/app/api/tools/workday/change-job/route.ts b/apps/sim/app/api/tools/workday/change-job/route.ts index 8cbba58fe1f..4af97d796ed 100644 --- a/apps/sim/app/api/tools/workday/change-job/route.ts +++ b/apps/sim/app/api/tools/workday/change-job/route.ts @@ -28,20 +28,24 @@ export const POST = withRouteHandler(async (request: NextRequest) => { const changeJobDetailData: Record = { Reason_Reference: wdRef('Change_Job_Subcategory_ID', data.reason), } + if (data.newSupervisoryOrgId) { + changeJobDetailData.Supervisory_Organization_Reference = wdRef( + 'Organization_Reference_ID', + data.newSupervisoryOrgId + ) + } if (data.newPositionId) { - changeJobDetailData.Position_Reference = wdRef('Position_ID', data.newPositionId) + changeJobDetailData.Proposed_Position_Reference = wdRef('Position_ID', data.newPositionId) } + const jobDetailsData: Record = {} if (data.newJobProfileId) { - changeJobDetailData.Job_Profile_Reference = wdRef('Job_Profile_ID', data.newJobProfileId) + jobDetailsData.Job_Profile_Reference = wdRef('Job_Profile_ID', data.newJobProfileId) } if (data.newLocationId) { - changeJobDetailData.Location_Reference = wdRef('Location_ID', data.newLocationId) + jobDetailsData.Location_Reference = wdRef('Location_ID', data.newLocationId) } - if (data.newSupervisoryOrgId) { - changeJobDetailData.Supervisory_Organization_Reference = wdRef( - 'Supervisory_Organization_ID', - data.newSupervisoryOrgId - ) + if (Object.keys(jobDetailsData).length > 0) { + changeJobDetailData.Job_Details_Data = jobDetailsData } const client = await createWorkdaySoapClient( diff --git a/apps/sim/app/api/tools/workday/update-worker/route.ts b/apps/sim/app/api/tools/workday/update-worker/route.ts index e3681a2567e..e3e5f7757dc 100644 --- a/apps/sim/app/api/tools/workday/update-worker/route.ts +++ b/apps/sim/app/api/tools/workday/update-worker/route.ts @@ -38,7 +38,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => { Auto_Complete: true, Run_Now: true, }, - Change_Personal_Information_Data: { + Change_Personal_Information_Business_Process_Data: { Person_Reference: wdRef('Employee_ID', data.workerId), Personal_Information_Data: data.fields, }, diff --git a/apps/sim/blocks/blocks/brightdata.ts b/apps/sim/blocks/blocks/brightdata.ts index b9cbe04109a..8accdb0c447 100644 --- a/apps/sim/blocks/blocks/brightdata.ts +++ b/apps/sim/blocks/blocks/brightdata.ts @@ -13,7 +13,7 @@ export const BrightDataBlock: BlockConfig = { docsLink: 'https://docs.sim.ai/tools/brightdata', category: 'tools', integrationType: IntegrationType.Search, - bgColor: '#0F4C81', + bgColor: '#FFFFFF', icon: BrightDataIcon, subBlocks: [ { diff --git a/apps/sim/blocks/blocks/databricks.ts b/apps/sim/blocks/blocks/databricks.ts index e2fe34f045e..e60d29f80f0 100644 --- a/apps/sim/blocks/blocks/databricks.ts +++ b/apps/sim/blocks/blocks/databricks.ts @@ -22,13 +22,17 @@ export const DatabricksBlock: BlockConfig = { type: 'dropdown', options: [ { label: 'Execute SQL', id: 'execute_sql' }, + { label: 'Get Statement', id: 'get_statement' }, + { label: 'List Warehouses', id: 'list_warehouses' }, { label: 'List Jobs', id: 'list_jobs' }, + { label: 'Get Job', id: 'get_job' }, { label: 'Run Job', id: 'run_job' }, { label: 'Get Run', id: 'get_run' }, { label: 'List Runs', id: 'list_runs' }, { label: 'Cancel Run', id: 'cancel_run' }, { label: 'Get Run Output', id: 'get_run_output' }, { label: 'List Clusters', id: 'list_clusters' }, + { label: 'Get Cluster', id: 'get_cluster' }, ], value: () => 'execute_sql', }, @@ -83,6 +87,16 @@ export const DatabricksBlock: BlockConfig = { mode: 'advanced', }, + // ── Get Statement ── + { + id: 'statementId', + title: 'Statement ID', + type: 'short-input', + placeholder: 'Enter the statement ID', + condition: { field: 'operation', value: 'get_statement' }, + required: { field: 'operation', value: 'get_statement' }, + }, + // ── List Jobs ── { id: 'name', @@ -126,8 +140,8 @@ export const DatabricksBlock: BlockConfig = { title: 'Job ID', type: 'short-input', placeholder: 'Enter the job ID', - condition: { field: 'operation', value: ['run_job', 'list_runs'] }, - required: { field: 'operation', value: 'run_job' }, + condition: { field: 'operation', value: ['run_job', 'list_runs', 'get_job'] }, + required: { field: 'operation', value: ['run_job', 'get_job'] }, }, { id: 'jobParameters', @@ -295,6 +309,16 @@ Return ONLY the numeric timestamp in milliseconds - no explanations, no extra te }, }, + // ── Get Cluster ── + { + id: 'clusterId', + title: 'Cluster ID', + type: 'short-input', + placeholder: 'Enter the cluster ID', + condition: { field: 'operation', value: 'get_cluster' }, + required: { field: 'operation', value: 'get_cluster' }, + }, + // ── Credentials (common to all operations) ── { id: 'host', @@ -315,13 +339,17 @@ Return ONLY the numeric timestamp in milliseconds - no explanations, no extra te tools: { access: [ 'databricks_execute_sql', + 'databricks_get_statement', + 'databricks_list_warehouses', 'databricks_list_jobs', + 'databricks_get_job', 'databricks_run_job', 'databricks_get_run', 'databricks_list_runs', 'databricks_cancel_run', 'databricks_get_run_output', 'databricks_list_clusters', + 'databricks_get_cluster', ], config: { tool: (params) => `databricks_${params.operation}`, @@ -350,6 +378,8 @@ Return ONLY the numeric timestamp in milliseconds - no explanations, no extra te apiKey: { type: 'string', description: 'Databricks Personal Access Token' }, warehouseId: { type: 'string', description: 'SQL warehouse ID' }, statement: { type: 'string', description: 'SQL statement to execute' }, + statementId: { type: 'string', description: 'Statement ID to poll for results' }, + clusterId: { type: 'string', description: 'Cluster ID to retrieve' }, catalog: { type: 'string', description: 'Unity Catalog name' }, schema: { type: 'string', description: 'Schema name' }, rowLimit: { type: 'number', description: 'Maximum rows to return' }, @@ -383,6 +413,25 @@ Return ONLY the numeric timestamp in milliseconds - no explanations, no extra te jobs: { type: 'json', description: 'List of jobs' }, hasMore: { type: 'boolean', description: 'Whether more results are available' }, nextPageToken: { type: 'string', description: 'Pagination token for next page' }, + // List Warehouses + warehouses: { + type: 'json', + description: + 'List of SQL warehouses ([{warehouseId, name, clusterSize, state, warehouseType, ...}])', + }, + // Get Job + name: { type: 'string', description: 'Job name' }, + runAsUserName: { type: 'string', description: 'User the job runs as' }, + format: { type: 'string', description: 'Job format (SINGLE_TASK or MULTI_TASK)' }, + maxConcurrentRuns: { type: 'number', description: 'Maximum number of concurrent runs' }, + timeoutSeconds: { type: 'number', description: 'Job-level timeout in seconds' }, + createdTime: { type: 'number', description: 'Job creation timestamp (epoch ms)' }, + schedule: { + type: 'json', + description: 'Cron schedule (quartz_cron_expression, timezone_id, pause_status)', + }, + tags: { type: 'json', description: 'Key-value tags applied to the job' }, + tasks: { type: 'json', description: 'Task definitions for the job' }, // Run Job runId: { type: 'number', description: 'Triggered run ID' }, numberInJob: { type: 'number', description: 'Run sequence number in job' }, @@ -415,6 +464,11 @@ Return ONLY the numeric timestamp in milliseconds - no explanations, no extra te logsTruncated: { type: 'boolean', description: 'Whether logs were truncated' }, // List Clusters clusters: { type: 'json', description: 'List of clusters' }, + // Get Cluster + cluster: { + type: 'json', + description: 'Cluster detail (clusterId, clusterName, state, sparkVersion, autoscale, ...)', + }, }, } diff --git a/apps/sim/blocks/blocks/dub.ts b/apps/sim/blocks/blocks/dub.ts index 9dcc902c80b..f2fa46ea26e 100644 --- a/apps/sim/blocks/blocks/dub.ts +++ b/apps/sim/blocks/blocks/dub.ts @@ -27,7 +27,13 @@ export const DubBlock: BlockConfig = { { label: 'Update Link', id: 'update_link' }, { label: 'Delete Link', id: 'delete_link' }, { label: 'List Links', id: 'list_links' }, + { label: 'Count Links', id: 'get_links_count' }, + { label: 'Bulk Create Links', id: 'bulk_create_links' }, + { label: 'Bulk Update Links', id: 'bulk_update_links' }, + { label: 'Bulk Delete Links', id: 'bulk_delete_links' }, { label: 'Get Analytics', id: 'get_analytics' }, + { label: 'List Events', id: 'get_events' }, + { label: 'Get QR Code', id: 'get_qr_code' }, ], value: () => 'create_link', }, @@ -158,6 +164,30 @@ export const DubBlock: BlockConfig = { condition: { field: 'operation', value: ['create_link', 'upsert_link', 'update_link'] }, mode: 'advanced', }, + { + id: 'linkRewrite', + title: 'Link Cloaking', + type: 'dropdown', + options: [ + { label: 'No', id: 'false' }, + { label: 'Yes', id: 'true' }, + ], + value: () => 'false', + condition: { field: 'operation', value: ['create_link', 'upsert_link', 'update_link'] }, + mode: 'advanced', + }, + { + id: 'linkArchived', + title: 'Archived', + type: 'dropdown', + options: [ + { label: 'No', id: 'false' }, + { label: 'Yes', id: 'true' }, + ], + value: () => 'false', + condition: { field: 'operation', value: ['create_link', 'upsert_link', 'update_link'] }, + mode: 'advanced', + }, { id: 'linkId', title: 'Link ID', @@ -232,32 +262,6 @@ export const DubBlock: BlockConfig = { condition: { field: 'operation', value: 'list_links' }, mode: 'advanced', }, - { - id: 'sortBy', - title: 'Sort By', - type: 'dropdown', - options: [ - { label: 'Created At', id: 'createdAt' }, - { label: 'Clicks', id: 'clicks' }, - { label: 'Sale Amount', id: 'saleAmount' }, - { label: 'Last Clicked', id: 'lastClicked' }, - ], - value: () => 'createdAt', - condition: { field: 'operation', value: 'list_links' }, - mode: 'advanced', - }, - { - id: 'sortOrder', - title: 'Sort Order', - type: 'dropdown', - options: [ - { label: 'Descending', id: 'desc' }, - { label: 'Ascending', id: 'asc' }, - ], - value: () => 'desc', - condition: { field: 'operation', value: 'list_links' }, - mode: 'advanced', - }, { id: 'page', title: 'Page', @@ -392,6 +396,316 @@ export const DubBlock: BlockConfig = { condition: { field: 'operation', value: 'get_analytics' }, mode: 'advanced', }, + { + id: 'countSearch', + title: 'Search', + type: 'short-input', + placeholder: 'Search links by slug or destination URL', + condition: { field: 'operation', value: 'get_links_count' }, + }, + { + id: 'countDomain', + title: 'Filter by Domain', + type: 'short-input', + placeholder: 'dub.sh', + condition: { field: 'operation', value: 'get_links_count' }, + mode: 'advanced', + }, + { + id: 'countTagIds', + title: 'Filter by Tag IDs', + type: 'short-input', + placeholder: 'Comma-separated tag IDs', + condition: { field: 'operation', value: 'get_links_count' }, + mode: 'advanced', + }, + { + id: 'countTagNames', + title: 'Filter by Tag Names', + type: 'short-input', + placeholder: 'Comma-separated tag names', + condition: { field: 'operation', value: 'get_links_count' }, + mode: 'advanced', + }, + { + id: 'countFolderId', + title: 'Filter by Folder ID', + type: 'short-input', + placeholder: 'Folder ID', + condition: { field: 'operation', value: 'get_links_count' }, + mode: 'advanced', + }, + { + id: 'countShowArchived', + title: 'Show Archived', + type: 'dropdown', + options: [ + { label: 'No', id: 'false' }, + { label: 'Yes', id: 'true' }, + ], + value: () => 'false', + condition: { field: 'operation', value: 'get_links_count' }, + mode: 'advanced', + }, + { + id: 'countGroupBy', + title: 'Group By', + type: 'dropdown', + options: [ + { label: 'None (total)', id: '' }, + { label: 'Domain', id: 'domain' }, + { label: 'Tag', id: 'tagId' }, + { label: 'User', id: 'userId' }, + { label: 'Folder', id: 'folderId' }, + ], + value: () => '', + condition: { field: 'operation', value: 'get_links_count' }, + mode: 'advanced', + }, + { + id: 'bulkLinks', + title: 'Links', + type: 'code', + language: 'json', + placeholder: '[\n { "url": "https://example.com", "key": "my-link" }\n]', + condition: { field: 'operation', value: 'bulk_create_links' }, + required: { field: 'operation', value: 'bulk_create_links' }, + wandConfig: { + enabled: true, + prompt: `Generate a JSON array of Dub link objects based on the user's description. Each object must include a "url" and may include "key", "domain", "tagIds" (array), and UTM fields. Return ONLY the JSON array - no explanations, no extra text.`, + placeholder: 'Describe the links to create (e.g., "links for these 5 product pages")...', + generationType: 'json-object', + }, + }, + { + id: 'bulkUpdateLinkIds', + title: 'Link IDs', + type: 'short-input', + placeholder: 'Comma-separated link IDs (max 100)', + condition: { field: 'operation', value: 'bulk_update_links' }, + }, + { + id: 'bulkUpdateExternalIds', + title: 'External IDs', + type: 'short-input', + placeholder: 'Comma-separated external IDs (used if no link IDs)', + condition: { field: 'operation', value: 'bulk_update_links' }, + mode: 'advanced', + }, + { + id: 'bulkUpdateData', + title: 'Update Data', + type: 'code', + language: 'json', + placeholder: '{\n "archived": true,\n "tagIds": ["tag_123"]\n}', + condition: { field: 'operation', value: 'bulk_update_links' }, + required: { field: 'operation', value: 'bulk_update_links' }, + wandConfig: { + enabled: true, + prompt: `Generate a JSON object of Dub link fields to update based on the user's description (e.g. archived, tagIds, expiresAt, comments, UTM fields). Return ONLY the JSON object - no explanations, no extra text.`, + placeholder: 'Describe the changes to apply (e.g., "archive them and add the Q3 tag")...', + generationType: 'json-object', + }, + }, + { + id: 'bulkDeleteLinkIds', + title: 'Link IDs', + type: 'short-input', + placeholder: 'Comma-separated link IDs (max 100)', + condition: { field: 'operation', value: 'bulk_delete_links' }, + required: { field: 'operation', value: 'bulk_delete_links' }, + }, + { + id: 'eventsEvent', + title: 'Event Type', + type: 'dropdown', + options: [ + { label: 'Clicks', id: 'clicks' }, + { label: 'Leads', id: 'leads' }, + { label: 'Sales', id: 'sales' }, + ], + value: () => 'clicks', + condition: { field: 'operation', value: 'get_events' }, + }, + { + id: 'eventsLinkId', + title: 'Link ID', + type: 'short-input', + placeholder: 'Filter events by link ID', + condition: { field: 'operation', value: 'get_events' }, + }, + { + id: 'eventsExternalId', + title: 'External ID', + type: 'short-input', + placeholder: 'Filter by external ID (prefix with ext_)', + condition: { field: 'operation', value: 'get_events' }, + mode: 'advanced', + }, + { + id: 'eventsDomain', + title: 'Domain', + type: 'short-input', + placeholder: 'Filter by domain', + condition: { field: 'operation', value: 'get_events' }, + mode: 'advanced', + }, + { + id: 'eventsInterval', + title: 'Interval', + type: 'dropdown', + options: [ + { label: '24 Hours', id: '24h' }, + { label: '7 Days', id: '7d' }, + { label: '30 Days', id: '30d' }, + { label: '90 Days', id: '90d' }, + { label: '1 Year', id: '1y' }, + { label: 'Month to Date', id: 'mtd' }, + { label: 'Quarter to Date', id: 'qtd' }, + { label: 'Year to Date', id: 'ytd' }, + { label: 'All Time', id: 'all' }, + ], + value: () => '24h', + condition: { field: 'operation', value: 'get_events' }, + }, + { + id: 'eventsStart', + title: 'Start Date', + type: 'short-input', + placeholder: 'ISO 8601 date (overrides interval)', + condition: { field: 'operation', value: 'get_events' }, + mode: 'advanced', + wandConfig: { + enabled: true, + prompt: `Generate an ISO 8601 timestamp based on the user's description. Return ONLY the timestamp string - no explanations, no extra text.`, + placeholder: 'Describe the start date (e.g., "7 days ago", "start of month")...', + generationType: 'timestamp', + }, + }, + { + id: 'eventsEnd', + title: 'End Date', + type: 'short-input', + placeholder: 'ISO 8601 date (defaults to now)', + condition: { field: 'operation', value: 'get_events' }, + mode: 'advanced', + wandConfig: { + enabled: true, + prompt: `Generate an ISO 8601 timestamp based on the user's description. Return ONLY the timestamp string - no explanations, no extra text.`, + placeholder: 'Describe the end date (e.g., "today", "end of last month")...', + generationType: 'timestamp', + }, + }, + { + id: 'eventsCountry', + title: 'Country', + type: 'short-input', + placeholder: 'ISO 3166-1 alpha-2 code (e.g., US)', + condition: { field: 'operation', value: 'get_events' }, + mode: 'advanced', + }, + { + id: 'eventsTimezone', + title: 'Timezone', + type: 'short-input', + placeholder: 'IANA timezone (e.g., America/New_York)', + condition: { field: 'operation', value: 'get_events' }, + mode: 'advanced', + }, + { + id: 'eventsSortOrder', + title: 'Sort Order', + type: 'dropdown', + options: [ + { label: 'Descending', id: 'desc' }, + { label: 'Ascending', id: 'asc' }, + ], + value: () => 'desc', + condition: { field: 'operation', value: 'get_events' }, + mode: 'advanced', + }, + { + id: 'eventsPage', + title: 'Page', + type: 'short-input', + placeholder: '1', + condition: { field: 'operation', value: 'get_events' }, + mode: 'advanced', + }, + { + id: 'eventsLimit', + title: 'Limit', + type: 'short-input', + placeholder: '100 (max: 1000)', + condition: { field: 'operation', value: 'get_events' }, + mode: 'advanced', + }, + { + id: 'qrUrl', + title: 'Short Link URL', + type: 'short-input', + placeholder: 'https://dub.sh/my-link', + condition: { field: 'operation', value: 'get_qr_code' }, + required: { field: 'operation', value: 'get_qr_code' }, + }, + { + id: 'qrSize', + title: 'Size (px)', + type: 'short-input', + placeholder: '600', + condition: { field: 'operation', value: 'get_qr_code' }, + mode: 'advanced', + }, + { + id: 'qrLevel', + title: 'Error Correction', + type: 'dropdown', + options: [ + { label: 'Low (L)', id: 'L' }, + { label: 'Medium (M)', id: 'M' }, + { label: 'Quartile (Q)', id: 'Q' }, + { label: 'High (H)', id: 'H' }, + ], + value: () => 'L', + condition: { field: 'operation', value: 'get_qr_code' }, + mode: 'advanced', + }, + { + id: 'qrFgColor', + title: 'Foreground Color', + type: 'short-input', + placeholder: '#000000', + condition: { field: 'operation', value: 'get_qr_code' }, + mode: 'advanced', + }, + { + id: 'qrBgColor', + title: 'Background Color', + type: 'short-input', + placeholder: '#FFFFFF', + condition: { field: 'operation', value: 'get_qr_code' }, + mode: 'advanced', + }, + { + id: 'qrHideLogo', + title: 'Hide Logo', + type: 'dropdown', + options: [ + { label: 'No', id: 'false' }, + { label: 'Yes', id: 'true' }, + ], + value: () => 'false', + condition: { field: 'operation', value: 'get_qr_code' }, + mode: 'advanced', + }, + { + id: 'qrMargin', + title: 'Margin', + type: 'short-input', + placeholder: '2', + condition: { field: 'operation', value: 'get_qr_code' }, + mode: 'advanced', + }, { id: 'apiKey', title: 'API Key', @@ -409,12 +723,26 @@ export const DubBlock: BlockConfig = { 'dub_update_link', 'dub_delete_link', 'dub_list_links', + 'dub_get_links_count', + 'dub_bulk_create_links', + 'dub_bulk_update_links', + 'dub_bulk_delete_links', 'dub_get_analytics', + 'dub_get_events', + 'dub_get_qr_code', ], config: { tool: (params) => `dub_${params.operation}`, params: (params) => { const result: Record = {} + if ( + params.operation === 'create_link' || + params.operation === 'upsert_link' || + params.operation === 'update_link' + ) { + if (params.linkRewrite === 'true') result.rewrite = true + if (params.linkArchived === 'true') result.archived = true + } if (params.operation === 'get_link') { if (params.getLinkExternalId) result.externalId = params.getLinkExternalId if (params.getLinkDomain) result.domain = params.getLinkDomain @@ -442,6 +770,49 @@ export const DubBlock: BlockConfig = { if (params.analyticsCountry) result.country = params.analyticsCountry if (params.analyticsTimezone) result.timezone = params.analyticsTimezone } + if (params.operation === 'get_links_count') { + if (params.countSearch) result.search = params.countSearch + if (params.countDomain) result.domain = params.countDomain + if (params.countTagIds) result.tagIds = params.countTagIds + if (params.countTagNames) result.tagNames = params.countTagNames + if (params.countFolderId) result.folderId = params.countFolderId + if (params.countShowArchived === 'true') result.showArchived = true + if (params.countGroupBy) result.groupBy = params.countGroupBy + } + if (params.operation === 'bulk_create_links') { + if (params.bulkLinks) result.links = params.bulkLinks + } + if (params.operation === 'bulk_update_links') { + if (params.bulkUpdateLinkIds) result.linkIds = params.bulkUpdateLinkIds + if (params.bulkUpdateExternalIds) result.externalIds = params.bulkUpdateExternalIds + if (params.bulkUpdateData) result.data = params.bulkUpdateData + } + if (params.operation === 'bulk_delete_links') { + if (params.bulkDeleteLinkIds) result.linkIds = params.bulkDeleteLinkIds + } + if (params.operation === 'get_events') { + if (params.eventsEvent) result.event = params.eventsEvent + if (params.eventsLinkId) result.linkId = params.eventsLinkId + if (params.eventsExternalId) result.externalId = params.eventsExternalId + if (params.eventsDomain) result.domain = params.eventsDomain + if (params.eventsInterval) result.interval = params.eventsInterval + if (params.eventsStart) result.start = params.eventsStart + if (params.eventsEnd) result.end = params.eventsEnd + if (params.eventsCountry) result.country = params.eventsCountry + if (params.eventsTimezone) result.timezone = params.eventsTimezone + if (params.eventsSortOrder) result.sortOrder = params.eventsSortOrder + if (params.eventsPage) result.page = Number(params.eventsPage) + if (params.eventsLimit) result.limit = Number(params.eventsLimit) + } + if (params.operation === 'get_qr_code') { + if (params.qrUrl) result.url = params.qrUrl + if (params.qrSize) result.size = Number(params.qrSize) + if (params.qrLevel) result.level = params.qrLevel + if (params.qrFgColor) result.fgColor = params.qrFgColor + if (params.qrBgColor) result.bgColor = params.qrBgColor + if (params.qrHideLogo === 'true') result.hideLogo = true + if (params.qrMargin) result.margin = Number(params.qrMargin) + } return result }, }, @@ -454,6 +825,9 @@ export const DubBlock: BlockConfig = { domain: { type: 'string', description: 'Custom domain for the short link' }, key: { type: 'string', description: 'Custom slug for the short link' }, search: { type: 'string', description: 'Search query for listing links' }, + links: { type: 'json', description: 'JSON array of link objects for bulk create' }, + data: { type: 'json', description: 'JSON object of fields to apply for bulk update' }, + linkIds: { type: 'string', description: 'Comma-separated link IDs for bulk operations' }, }, outputs: { id: { type: 'string', description: 'Link ID' }, @@ -483,11 +857,34 @@ export const DubBlock: BlockConfig = { type: 'json', description: 'Array of links (id, domain, key, url, shortLink, clicks, tags, createdAt)', }, - count: { type: 'number', description: 'Number of links returned (list operation)' }, + count: { type: 'number', description: 'Number of items returned (list/count/events/bulk)' }, data: { type: 'json', description: 'Grouped analytics data (timeseries, countries, devices, etc.)', }, + groups: { + type: 'json', + description: 'Per-group link counts when Count Links uses groupBy ([{ field, count }])', + }, + events: { + type: 'json', + description: 'Array of events (event, timestamp, click, link, customer/sale data)', + }, + created: { + type: 'json', + description: 'Bulk create: array of successfully created link objects', + }, + errors: { + type: 'json', + description: 'Bulk create: array of per-link errors ({ link, error, code })', + }, + updated: { + type: 'json', + description: 'Bulk update: array of updated link objects', + }, + deletedCount: { type: 'number', description: 'Bulk delete: number of links deleted' }, + file: { type: 'file', description: 'QR code image (PNG) stored in execution files' }, + content: { type: 'string', description: 'QR code as base64-encoded PNG data' }, }, } diff --git a/apps/sim/blocks/blocks/duckduckgo.ts b/apps/sim/blocks/blocks/duckduckgo.ts index 314c3447d7f..1ec84d5cb72 100644 --- a/apps/sim/blocks/blocks/duckduckgo.ts +++ b/apps/sim/blocks/blocks/duckduckgo.ts @@ -27,11 +27,13 @@ export const DuckDuckGoBlock: BlockConfig = { title: 'Remove HTML', type: 'switch', defaultValue: true, + mode: 'advanced', }, { id: 'skipDisambig', title: 'Skip Disambiguation', type: 'switch', + mode: 'advanced', }, ], tools: { @@ -51,10 +53,17 @@ export const DuckDuckGoBlock: BlockConfig = { abstractText: { type: 'string', description: 'Plain text version of the abstract' }, abstractSource: { type: 'string', description: 'The source of the abstract' }, abstractURL: { type: 'string', description: 'URL to the source of the abstract' }, + definition: { type: 'string', description: 'Dictionary-style definition if available' }, + definitionSource: { type: 'string', description: 'The source of the definition' }, + definitionURL: { type: 'string', description: 'URL to the source of the definition' }, image: { type: 'string', description: 'URL to an image related to the topic' }, answer: { type: 'string', description: 'Direct answer if available' }, answerType: { type: 'string', description: 'Type of the answer' }, type: { type: 'string', description: 'Response type (A, D, C, N, E)' }, + redirect: { + type: 'string', + description: '!bang redirect URL, populated only for bang queries', + }, relatedTopics: { type: 'json', description: 'Array of related topics' }, results: { type: 'json', description: 'Array of external link results' }, }, diff --git a/apps/sim/blocks/blocks/enrich.ts b/apps/sim/blocks/blocks/enrich.ts index aaf887d28b1..fcefd461ed3 100644 --- a/apps/sim/blocks/blocks/enrich.ts +++ b/apps/sim/blocks/blocks/enrich.ts @@ -47,11 +47,14 @@ export const EnrichBlock: BlockConfig = { { label: 'Search Company Employees', id: 'search_company_employees' }, { label: 'Search Similar Companies', id: 'search_similar_companies' }, { label: 'Sales Pointer (People)', id: 'sales_pointer_people' }, + { label: 'Search Jobs', id: 'search_jobs' }, // LinkedIn Posts/Activities { label: 'Search Posts', id: 'search_posts' }, { label: 'Get Post Details', id: 'get_post_details' }, { label: 'Search Post Reactions', id: 'search_post_reactions' }, + { label: 'Search Post Reactions (by URL)', id: 'search_post_reactions_by_url' }, { label: 'Search Post Comments', id: 'search_post_comments' }, + { label: 'Search Post Comments (by URL)', id: 'search_post_comments_by_url' }, { label: 'Search People Activities', id: 'search_people_activities' }, { label: 'Search Company Activities', id: 'search_company_activities' }, // Other @@ -351,8 +354,8 @@ export const EnrichBlock: BlockConfig = { title: 'Keywords', type: 'short-input', placeholder: 'AI automation', - condition: { field: 'operation', value: 'search_posts' }, - required: { field: 'operation', value: 'search_posts' }, + condition: { field: 'operation', value: ['search_posts', 'search_jobs'] }, + required: { field: 'operation', value: ['search_posts', 'search_jobs'] }, }, { id: 'datePosted', @@ -367,13 +370,103 @@ export const EnrichBlock: BlockConfig = { condition: { field: 'operation', value: 'search_posts' }, }, + { + id: 'jobLocation', + title: 'Location', + type: 'short-input', + placeholder: 'London', + condition: { field: 'operation', value: 'search_jobs' }, + }, + { + id: 'timePosted', + title: 'Time Posted', + type: 'dropdown', + options: [ + { label: 'Any time', id: '' }, + { label: 'Past 24 hours', id: 'past_24hrs' }, + { label: 'Past week', id: 'past_week' }, + { label: 'Past month', id: 'past_month' }, + ], + condition: { field: 'operation', value: 'search_jobs' }, + }, + { + id: 'jobTypes', + title: 'Job Types', + type: 'short-input', + placeholder: 'full time, part time', + condition: { field: 'operation', value: 'search_jobs' }, + mode: 'advanced', + wandConfig: { + enabled: true, + placeholder: 'Describe the job types to include', + prompt: + 'Convert the request into a comma-separated list of LinkedIn job types (e.g., full time, part time, contract, internship, temporary). Return ONLY the comma-separated list - no explanations, no extra text.', + }, + }, + { + id: 'workplaceTypes', + title: 'Workplace Types', + type: 'short-input', + placeholder: 'on site, remote', + condition: { field: 'operation', value: 'search_jobs' }, + mode: 'advanced', + wandConfig: { + enabled: true, + placeholder: 'Describe the workplace types to include', + prompt: + 'Convert the request into a comma-separated list of LinkedIn workplace types (on site, remote, hybrid). Return ONLY the comma-separated list - no explanations, no extra text.', + }, + }, + { + id: 'experienceLevels', + title: 'Experience Levels', + type: 'short-input', + placeholder: 'internship, associate', + condition: { field: 'operation', value: 'search_jobs' }, + mode: 'advanced', + wandConfig: { + enabled: true, + placeholder: 'Describe the experience levels to include', + prompt: + 'Convert the request into a comma-separated list of LinkedIn experience levels (internship, entry level, associate, mid-senior level, director, executive). Return ONLY the comma-separated list - no explanations, no extra text.', + }, + }, + { + id: 'jobCompanyIds', + title: 'Company IDs', + type: 'short-input', + placeholder: '2048, 3050', + condition: { field: 'operation', value: 'search_jobs' }, + mode: 'advanced', + wandConfig: { + enabled: true, + placeholder: 'Describe the companies to filter by', + prompt: + 'Convert the request into a comma-separated list of LinkedIn company IDs (numeric). Return ONLY the comma-separated list - no explanations, no extra text.', + }, + }, + { + id: 'start', + title: 'Start Offset', + type: 'short-input', + placeholder: '0', + condition: { field: 'operation', value: 'search_jobs' }, + mode: 'advanced', + }, + { id: 'postUrl', title: 'LinkedIn Post URL', type: 'short-input', placeholder: 'https://www.linkedin.com/posts/...', - condition: { field: 'operation', value: 'get_post_details' }, - required: { field: 'operation', value: 'get_post_details' }, + condition: { + field: 'operation', + value: ['get_post_details', 'search_post_reactions_by_url', 'search_post_comments_by_url'], + }, + required: { + field: 'operation', + value: ['get_post_details', 'search_post_reactions_by_url', 'search_post_comments_by_url'], + }, }, { @@ -402,7 +495,11 @@ export const EnrichBlock: BlockConfig = { { label: 'Insightful', id: 'insightful' }, { label: 'Funny', id: 'funny' }, ], - condition: { field: 'operation', value: 'search_post_reactions' }, + value: () => 'all', + condition: { + field: 'operation', + value: ['search_post_reactions', 'search_post_reactions_by_url'], + }, }, { @@ -422,6 +519,7 @@ export const EnrichBlock: BlockConfig = { { label: 'Comments', id: 'comments' }, { label: 'Articles', id: 'articles' }, ], + value: () => 'posts', condition: { field: 'operation', value: ['search_people_activities', 'search_company_activities'], @@ -469,10 +567,15 @@ export const EnrichBlock: BlockConfig = { 'sales_pointer_people', 'search_posts', 'search_post_reactions', + 'search_post_reactions_by_url', 'search_post_comments', + 'search_post_comments_by_url', ], }, - required: { field: 'operation', value: 'sales_pointer_people' }, + required: { + field: 'operation', + value: ['sales_pointer_people', 'search_post_reactions', 'search_post_reactions_by_url'], + }, }, { id: 'pageSize', @@ -519,10 +622,13 @@ export const EnrichBlock: BlockConfig = { 'enrich_search_company_employees', 'enrich_search_similar_companies', 'enrich_sales_pointer_people', + 'enrich_search_jobs', 'enrich_search_posts', 'enrich_get_post_details', 'enrich_search_post_reactions', + 'enrich_search_post_reactions_by_url', 'enrich_search_post_comments', + 'enrich_search_post_comments_by_url', 'enrich_search_people_activities', 'enrich_search_company_activities', 'enrich_reverse_hash_lookup', @@ -594,6 +700,12 @@ export const EnrichBlock: BlockConfig = { if (operation === 'search_logo') { parsedParams.url = rest.domain } + if (operation === 'search_jobs') { + parsedParams.location = rest.jobLocation + parsedParams.jobLocation = undefined + parsedParams.companyIds = rest.jobCompanyIds + parsedParams.jobCompanyIds = undefined + } if (parsedParams.page) { const pageNum = Number(parsedParams.page) @@ -607,6 +719,7 @@ export const EnrichBlock: BlockConfig = { if (parsedParams.pageSize) parsedParams.pageSize = Number(parsedParams.pageSize) if (parsedParams.num) parsedParams.num = Number(parsedParams.num) if (parsedParams.offset) parsedParams.offset = Number(parsedParams.offset) + if (parsedParams.start) parsedParams.start = Number(parsedParams.start) if (parsedParams.staffCountMin) parsedParams.staffCountMin = Number(parsedParams.staffCountMin) if (parsedParams.staffCountMax) diff --git a/apps/sim/blocks/blocks/fireflies.ts b/apps/sim/blocks/blocks/fireflies.ts index 674058943ba..88a333e14db 100644 --- a/apps/sim/blocks/blocks/fireflies.ts +++ b/apps/sim/blocks/blocks/fireflies.ts @@ -179,6 +179,18 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, value: ['fireflies_list_transcripts', 'fireflies_list_bites'], }, }, + { + id: 'skip', + title: 'Skip', + type: 'short-input', + placeholder: 'Number of results to skip (default: 0)', + required: false, + mode: 'advanced', + condition: { + field: 'operation', + value: ['fireflies_list_transcripts', 'fireflies_list_bites'], + }, + }, // Upload Audio fields - File upload (basic mode) { id: 'audioFile', @@ -380,6 +392,21 @@ Return ONLY the valid JSON array - no explanations, no markdown code blocks.`, value: 'fireflies_create_bite', }, }, + { + id: 'mediaType', + title: 'Media Type', + type: 'dropdown', + options: [ + { label: 'Video', id: 'video' }, + { label: 'Audio', id: 'audio' }, + ], + required: false, + mode: 'advanced', + condition: { + field: 'operation', + value: 'fireflies_create_bite', + }, + }, { id: 'biteName', title: 'Bite Name', @@ -453,6 +480,7 @@ Return ONLY the summary text - no quotes, no labels.`, hostEmail: params.hostEmail || undefined, participants: params.participants || undefined, limit: params.limit ? Number(params.limit) : undefined, + skip: params.skip ? Number(params.skip) : undefined, } case 'fireflies_get_transcript': @@ -528,6 +556,7 @@ Return ONLY the summary text - no quotes, no labels.`, startTime: Number(params.startTime), endTime: Number(params.endTime), name: params.biteName?.trim() || undefined, + mediaType: params.mediaType?.trim() || undefined, summary: params.biteSummary?.trim() || undefined, } @@ -537,6 +566,7 @@ Return ONLY the summary text - no quotes, no labels.`, transcriptId: params.transcriptId?.trim() || undefined, mine: true, limit: params.limit ? Number(params.limit) : undefined, + skip: params.skip ? Number(params.skip) : undefined, } case 'fireflies_list_contacts': @@ -559,6 +589,7 @@ Return ONLY the summary text - no quotes, no labels.`, hostEmail: { type: 'string', description: 'Filter by host email' }, participants: { type: 'string', description: 'Filter by participants (comma-separated)' }, limit: { type: 'number', description: 'Maximum results to return' }, + skip: { type: 'number', description: 'Number of results to skip for pagination' }, audioFile: { type: 'json', description: 'Audio/video file (canonical param)' }, audioUrl: { type: 'string', description: 'Public URL to audio file' }, title: { type: 'string', description: 'Meeting title' }, @@ -570,6 +601,7 @@ Return ONLY the summary text - no quotes, no labels.`, duration: { type: 'number', description: 'Meeting duration in minutes (15-120)' }, startTime: { type: 'number', description: 'Bite start time in seconds' }, endTime: { type: 'number', description: 'Bite end time in seconds' }, + mediaType: { type: 'string', description: 'Bite media type (video or audio)' }, biteName: { type: 'string', description: 'Name for the bite/highlight' }, biteSummary: { type: 'string', description: 'Summary for the bite' }, }, diff --git a/apps/sim/blocks/blocks/gong.ts b/apps/sim/blocks/blocks/gong.ts index 223f7883210..ccadd9183bf 100644 --- a/apps/sim/blocks/blocks/gong.ts +++ b/apps/sim/blocks/blocks/gong.ts @@ -33,6 +33,8 @@ export const GongBlock: BlockConfig = { { label: 'List Users', id: 'list_users' }, { label: 'Get User', id: 'get_user' }, { label: 'Aggregate Activity', id: 'aggregate_activity' }, + { label: 'Day-by-Day Activity', id: 'day_by_day_activity' }, + { label: 'Aggregate by Period', id: 'aggregate_by_period' }, { label: 'Interaction Stats', id: 'interaction_stats' }, { label: 'Answered Scorecards', id: 'answered_scorecards' }, { label: 'List Library Folders', id: 'list_library_folders' }, @@ -319,8 +321,24 @@ Return ONLY the timestamp string in ISO 8601 format - no explanations, no quotes title: 'From Date', type: 'short-input', placeholder: '2024-01-01 (YYYY-MM-DD, inclusive)', - condition: { field: 'operation', value: ['aggregate_activity', 'interaction_stats'] }, - required: { field: 'operation', value: ['aggregate_activity', 'interaction_stats'] }, + condition: { + field: 'operation', + value: [ + 'aggregate_activity', + 'day_by_day_activity', + 'aggregate_by_period', + 'interaction_stats', + ], + }, + required: { + field: 'operation', + value: [ + 'aggregate_activity', + 'day_by_day_activity', + 'aggregate_by_period', + 'interaction_stats', + ], + }, wandConfig: { enabled: true, prompt: `Generate a date string in YYYY-MM-DD format based on the user's description. @@ -340,8 +358,24 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n title: 'To Date', type: 'short-input', placeholder: '2024-01-31 (YYYY-MM-DD, exclusive)', - condition: { field: 'operation', value: ['aggregate_activity', 'interaction_stats'] }, - required: { field: 'operation', value: ['aggregate_activity', 'interaction_stats'] }, + condition: { + field: 'operation', + value: [ + 'aggregate_activity', + 'day_by_day_activity', + 'aggregate_by_period', + 'interaction_stats', + ], + }, + required: { + field: 'operation', + value: [ + 'aggregate_activity', + 'day_by_day_activity', + 'aggregate_by_period', + 'interaction_stats', + ], + }, wandConfig: { enabled: true, prompt: `Generate a date string in YYYY-MM-DD format based on the user's description. @@ -361,10 +395,35 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n title: 'User IDs', type: 'short-input', placeholder: 'Comma-separated user IDs (optional)', - condition: { field: 'operation', value: ['aggregate_activity', 'interaction_stats'] }, + condition: { + field: 'operation', + value: [ + 'aggregate_activity', + 'day_by_day_activity', + 'aggregate_by_period', + 'interaction_stats', + ], + }, mode: 'advanced', }, + // Aggregate by Period inputs + { + id: 'aggregationPeriod', + title: 'Aggregation Period', + type: 'dropdown', + options: [ + { label: 'Day', id: 'DAY' }, + { label: 'Week', id: 'WEEK' }, + { label: 'Month', id: 'MONTH' }, + { label: 'Quarter', id: 'QUARTER' }, + { label: 'Year', id: 'YEAR' }, + ], + value: () => 'WEEK', + condition: { field: 'operation', value: 'aggregate_by_period' }, + required: { field: 'operation', value: 'aggregate_by_period' }, + }, + // Answered Scorecards inputs { id: 'callFromDate', @@ -585,6 +644,8 @@ Return ONLY the timestamp string in ISO 8601 format - no explanations, no quotes 'get_extensive_calls', 'list_users', 'aggregate_activity', + 'day_by_day_activity', + 'aggregate_by_period', 'interaction_stats', 'answered_scorecards', 'list_flows', @@ -621,6 +682,8 @@ Return ONLY the timestamp string in ISO 8601 format - no explanations, no quotes 'gong_list_users', 'gong_get_user', 'gong_aggregate_activity', + 'gong_day_by_day_activity', + 'gong_aggregate_by_period', 'gong_interaction_stats', 'gong_answered_scorecards', 'gong_list_library_folders', @@ -677,6 +740,10 @@ Return ONLY the timestamp string in ISO 8601 format - no explanations, no quotes callIds: { type: 'string', description: 'Comma-separated call IDs' }, userId: { type: 'string', description: 'Gong user ID' }, userIds: { type: 'string', description: 'Comma-separated user IDs' }, + aggregationPeriod: { + type: 'string', + description: 'Calendar period for aggregate-by-period (DAY/WEEK/MONTH/QUARTER/YEAR)', + }, statsFromDate: { type: 'string', description: 'Start date in YYYY-MM-DD format (stats)' }, statsToDate: { type: 'string', description: 'End date in YYYY-MM-DD format (stats)' }, callFromDate: { type: 'string', description: 'Call start date in YYYY-MM-DD (scorecards)' }, @@ -701,10 +768,152 @@ Return ONLY the timestamp string in ISO 8601 format - no explanations, no quotes cursor: { type: 'string', description: 'Pagination cursor' }, }, outputs: { - response: { + // Shared across most operations + requestId: { type: 'string', description: 'Gong request reference ID for troubleshooting' }, + cursor: { type: 'string', description: 'Pagination cursor for the next page' }, + totalRecords: { type: 'number', description: 'Total number of records matching the filter' }, + currentPageSize: { type: 'number', description: 'Number of records in the current page' }, + currentPageNumber: { type: 'number', description: 'Current page number' }, + + // list_calls / get_extensive_calls / get_folder_content / lookup_email / lookup_phone + calls: { + type: 'json', + description: + 'Calls returned by the operation (shape varies: call list, extensive calls, folder calls, or call references)', + }, + + // create_call / get_call + callId: { type: 'string', description: 'Gong call ID of the created call' }, + url: { type: 'string', description: 'URL to the call in the Gong web app' }, + id: { type: 'string', description: 'Gong ID of the returned call or user' }, + title: { type: 'string', description: 'Call title' }, + scheduled: { type: 'string', description: 'Scheduled call time (ISO-8601)' }, + started: { type: 'string', description: 'Recording start time (ISO-8601)' }, + duration: { type: 'number', description: 'Call duration in seconds' }, + direction: { type: 'string', description: 'Call direction (Inbound/Outbound)' }, + system: { type: 'string', description: 'Communication platform used' }, + scope: { type: 'string', description: "Call scope: 'Internal', 'External', or 'Unknown'" }, + media: { type: 'string', description: 'Media type (e.g., Video)' }, + language: { type: 'string', description: 'Language code (ISO-639-2B)' }, + primaryUserId: { type: 'string', description: 'Host team member identifier' }, + workspaceId: { type: 'string', description: 'Workspace identifier' }, + sdrDisposition: { type: 'string', description: 'SDR disposition classification' }, + clientUniqueId: { + type: 'string', + description: 'Call identifier from the origin recording system', + }, + customData: { type: 'string', description: 'Metadata provided during call creation' }, + purpose: { type: 'string', description: 'Call purpose' }, + meetingUrl: { type: 'string', description: 'Web conference provider URL' }, + isPrivate: { type: 'boolean', description: 'Whether the call is private' }, + calendarEventId: { type: 'string', description: 'Calendar event identifier' }, + + // get_call_transcript + callTranscripts: { type: 'json', description: - 'Gong API response data. Shape depends on the selected operation and can include callId, requestId, url, calls, callTranscripts, users, usersActivity, peopleInteractionStats, answeredScorecards, folders, scorecards, trackers, workspaces, flows, coachingData, emails, meetings, customerData, and pagination cursor fields.', + 'Call transcripts: [{callId, transcript: [{speakerId, topic, sentences: [{start, end, text}]}]}]', + }, + + // list_users / get_user + users: { type: 'json', description: 'List of Gong users with profile and settings fields' }, + emailAddress: { type: 'string', description: 'User email address' }, + created: { type: 'string', description: 'User creation timestamp (ISO-8601)' }, + active: { type: 'boolean', description: 'Whether the user is active' }, + emailAliases: { type: 'json', description: "User's alternative email addresses" }, + trustedEmailAddress: { type: 'string', description: 'Trusted email address for the user' }, + firstName: { type: 'string', description: 'User first name' }, + lastName: { type: 'string', description: 'User last name' }, + phoneNumber: { type: 'string', description: 'User phone number' }, + extension: { type: 'string', description: 'Phone extension number' }, + personalMeetingUrls: { type: 'json', description: 'Personal meeting URLs' }, + settings: { type: 'json', description: 'User settings (recording, import, and consent flags)' }, + managerId: { type: 'string', description: 'Manager user ID' }, + meetingConsentPageUrl: { type: 'string', description: 'Meeting consent page URL' }, + spokenLanguages: { type: 'json', description: 'Languages spoken: [{language, primary}]' }, + + // aggregate_activity / interaction_stats / day_by_day_activity / aggregate_by_period + usersActivity: { type: 'json', description: 'Aggregated activity stats per user' }, + usersDetailedActivities: { + type: 'json', + description: 'Day-by-day activity per user: call IDs grouped by activity type per day', + }, + usersAggregateActivity: { + type: 'json', + description: 'Aggregated activity per user grouped into time periods (with fromDate/toDate)', + }, + peopleInteractionStats: { + type: 'json', + description: + 'Interaction stats per user: [{userId, userEmailAddress, personInteractionStats: [{name, value}]}]', + }, + timeZone: { type: 'string', description: "The company's defined timezone in Gong" }, + fromDateTime: { type: 'string', description: 'Start of results (ISO-8601)' }, + toDateTime: { type: 'string', description: 'End of results (ISO-8601)' }, + + // answered_scorecards + answeredScorecards: { + type: 'json', + description: 'Answered scorecards with scores and answers', + }, + + // list_library_folders / get_folder_content + folders: { + type: 'json', + description: 'Library folders: [{id, name, parentFolderId, createdBy, updated}]', + }, + folderId: { type: 'string', description: 'Library folder ID' }, + folderName: { type: 'string', description: 'Library folder display name' }, + createdBy: { type: 'string', description: 'User ID who created the folder' }, + updated: { type: 'string', description: "Folder's last update time (ISO-8601)" }, + + // list_scorecards + scorecards: { type: 'json', description: 'Scorecard definitions with questions' }, + + // list_trackers + trackers: { type: 'json', description: 'Keyword/smart tracker definitions' }, + + // list_workspaces + workspaces: { type: 'json', description: 'Gong workspaces: [{id, name, description}]' }, + + // list_flows + flows: { + type: 'json', + description: + 'Gong Engage flows: [{id, name, folderId, folderName, visibility, creationDate, exclusive}]', + }, + + // get_coaching + coachingData: { + type: 'json', + description: "Coaching data per manager's team with direct-report metrics", + }, + + // lookup_email / lookup_phone + emails: { + type: 'json', + description: 'Related email messages: [{id, from, sentTime, mailbox, messageHash}]', + }, + meetings: { type: 'json', description: 'Related meetings: [{id}]' }, + customerData: { + type: 'json', + description: 'Linked external-system (CRM) objects referencing the contact', + }, + customerEngagement: { + type: 'json', + description: 'Customer engagement events (e.g., viewing shared calls)', + }, + suppliedPhoneNumber: { + type: 'string', + description: 'The phone number supplied in the lookup request', + }, + matchingPhoneNumbers: { + type: 'json', + description: 'Phone numbers in the system matching the supplied number', + }, + emailAddresses: { + type: 'json', + description: 'Email addresses associated with the phone number', }, }, triggers: { diff --git a/apps/sim/blocks/blocks/google_pagespeed.ts b/apps/sim/blocks/blocks/google_pagespeed.ts index 48d7d04a893..9f0d70841db 100644 --- a/apps/sim/blocks/blocks/google_pagespeed.ts +++ b/apps/sim/blocks/blocks/google_pagespeed.ts @@ -79,10 +79,49 @@ export const GooglePagespeedBlock: BlockConfig = }, outputs: { - response: { - type: 'json', - description: - 'PageSpeed analysis results including category scores (performanceScore, accessibilityScore, bestPracticesScore, seoScore), Core Web Vitals display values and numeric values (firstContentfulPaint, largestContentfulPaint, totalBlockingTime, cumulativeLayoutShift, speedIndex, interactive), and metadata (finalUrl, overallCategory, analysisTimestamp, lighthouseVersion)', + finalUrl: { type: 'string', description: 'The final URL after redirects' }, + performanceScore: { type: 'number', description: 'Performance category score (0-1)' }, + accessibilityScore: { type: 'number', description: 'Accessibility category score (0-1)' }, + bestPracticesScore: { type: 'number', description: 'Best Practices category score (0-1)' }, + seoScore: { type: 'number', description: 'SEO category score (0-1)' }, + firstContentfulPaint: { + type: 'string', + description: 'Time to First Contentful Paint (display value)', + }, + firstContentfulPaintMs: { + type: 'number', + description: 'Time to First Contentful Paint in milliseconds', + }, + largestContentfulPaint: { + type: 'string', + description: 'Time to Largest Contentful Paint (display value)', + }, + largestContentfulPaintMs: { + type: 'number', + description: 'Time to Largest Contentful Paint in milliseconds', + }, + totalBlockingTime: { type: 'string', description: 'Total Blocking Time (display value)' }, + totalBlockingTimeMs: { type: 'number', description: 'Total Blocking Time in milliseconds' }, + cumulativeLayoutShift: { + type: 'string', + description: 'Cumulative Layout Shift (display value)', + }, + cumulativeLayoutShiftValue: { + type: 'number', + description: 'Cumulative Layout Shift numeric value', + }, + speedIndex: { type: 'string', description: 'Speed Index (display value)' }, + speedIndexMs: { type: 'number', description: 'Speed Index in milliseconds' }, + interactive: { type: 'string', description: 'Time to Interactive (display value)' }, + interactiveMs: { type: 'number', description: 'Time to Interactive in milliseconds' }, + overallCategory: { + type: 'string', + description: 'Overall loading experience category (FAST, AVERAGE, SLOW, or NONE)', + }, + analysisTimestamp: { type: 'string', description: 'UTC timestamp of the analysis' }, + lighthouseVersion: { + type: 'string', + description: 'Version of Lighthouse used for the analysis', }, }, } diff --git a/apps/sim/blocks/blocks/intercom.ts b/apps/sim/blocks/blocks/intercom.ts index d8645a8f10d..67906026320 100644 --- a/apps/sim/blocks/blocks/intercom.ts +++ b/apps/sim/blocks/blocks/intercom.ts @@ -15,7 +15,7 @@ export const IntercomBlock: BlockConfig = { authMode: AuthMode.ApiKey, category: 'tools', integrationType: IntegrationType.Support, - bgColor: '#1F8DED', + bgColor: '#FFFFFF', icon: IntercomIcon, subBlocks: [ { diff --git a/apps/sim/blocks/blocks/mailchimp.ts b/apps/sim/blocks/blocks/mailchimp.ts index 9650d7676ef..e39fe913271 100644 --- a/apps/sim/blocks/blocks/mailchimp.ts +++ b/apps/sim/blocks/blocks/mailchimp.ts @@ -13,7 +13,7 @@ export const MailchimpBlock: BlockConfig = { authMode: AuthMode.ApiKey, category: 'tools', integrationType: IntegrationType.Email, - bgColor: '#1A1A1A', + bgColor: '#FFE01B', icon: MailchimpIcon, subBlocks: [ { diff --git a/apps/sim/blocks/blocks/millionverifier.ts b/apps/sim/blocks/blocks/millionverifier.ts index dcadf72a7c3..66833d89950 100644 --- a/apps/sim/blocks/blocks/millionverifier.ts +++ b/apps/sim/blocks/blocks/millionverifier.ts @@ -12,7 +12,7 @@ export const MillionVerifierBlock: BlockConfig = { docsLink: 'https://docs.sim.ai/tools/millionverifier', category: 'tools', integrationType: IntegrationType.Sales, - bgColor: '#00B67A', + bgColor: '#FFFFFF', icon: MillionVerifierIcon, subBlocks: [ { diff --git a/apps/sim/blocks/blocks/okta.ts b/apps/sim/blocks/blocks/okta.ts index 02087c286cc..a9c81ac27d6 100644 --- a/apps/sim/blocks/blocks/okta.ts +++ b/apps/sim/blocks/blocks/okta.ts @@ -294,8 +294,11 @@ export const OktaBlock: BlockConfig = { if (params.groupName) result.name = params.groupName if (params.groupDescription !== undefined) result.description = params.groupDescription - // Pass through all other non-empty params - // Allow empty strings so users can clear fields (e.g. update_user partial updates) + // Pass through all other params, skipping empty values. Blank fields in a + // partial update (e.g. update_user, a POST merge) must be omitted so they + // leave the existing Okta value unchanged rather than overwriting it with + // an empty string. This mirrors the agent tool-call path, which already + // filters empty params before execution. const skipKeys = new Set([ 'operation', 'apiKey', @@ -305,7 +308,7 @@ export const OktaBlock: BlockConfig = { 'groupDescription', ]) for (const [key, value] of Object.entries(params)) { - if (!skipKeys.has(key) && value !== undefined && value !== null) { + if (!skipKeys.has(key) && value !== undefined && value !== null && value !== '') { result[key] = value } } diff --git a/apps/sim/blocks/blocks/servicenow.ts b/apps/sim/blocks/blocks/servicenow.ts index 2d05c2c36ea..3a6c23dacc2 100644 --- a/apps/sim/blocks/blocks/servicenow.ts +++ b/apps/sim/blocks/blocks/servicenow.ts @@ -1,6 +1,7 @@ import { ServiceNowIcon } from '@/components/icons' import type { BlockConfig, BlockMeta } from '@/blocks/types' -import { IntegrationType } from '@/blocks/types' +import { AuthMode, IntegrationType } from '@/blocks/types' +import { normalizeFileInput } from '@/blocks/utils' import type { ServiceNowResponse } from '@/tools/servicenow/types' import { getTrigger } from '@/triggers' @@ -13,6 +14,7 @@ export const ServiceNowBlock: BlockConfig = { docsLink: 'https://docs.sim.ai/tools/servicenow', category: 'tools', integrationType: IntegrationType.Support, + authMode: AuthMode.ApiKey, bgColor: '#032D42', icon: ServiceNowIcon, subBlocks: [ @@ -26,6 +28,10 @@ export const ServiceNowBlock: BlockConfig = { { label: 'Read Records', id: 'servicenow_read_record' }, { label: 'Update Record', id: 'servicenow_update_record' }, { label: 'Delete Record', id: 'servicenow_delete_record' }, + { label: 'Aggregate Records', id: 'servicenow_aggregate' }, + { label: 'List Attachments', id: 'servicenow_list_attachments' }, + { label: 'Download Attachment', id: 'servicenow_download_attachment' }, + { label: 'Upload Attachment', id: 'servicenow_upload_attachment' }, ], value: () => 'servicenow_read_record', }, @@ -57,12 +63,13 @@ export const ServiceNowBlock: BlockConfig = { required: true, description: 'Password for the ServiceNow user', }, - // Table Name + // Table Name (not needed for download attachment, which is addressed by attachment sys_id) { id: 'tableName', title: 'Table Name', type: 'short-input', placeholder: 'incident, task, sys_user, etc.', + condition: { field: 'operation', value: 'servicenow_download_attachment', not: true }, required: true, description: 'ServiceNow table name', }, @@ -120,7 +127,10 @@ Output: {"short_description": "Network outage", "description": "Network connecti title: 'Query String', type: 'short-input', placeholder: 'active=true^priority=1', - condition: { field: 'operation', value: 'servicenow_read_record' }, + condition: { + field: 'operation', + value: ['servicenow_read_record', 'servicenow_aggregate'], + }, description: 'ServiceNow encoded query string', mode: 'advanced', }, @@ -152,7 +162,10 @@ Output: {"short_description": "Network outage", "description": "Network connecti { label: 'All (both)', id: 'all' }, ], value: () => '', - condition: { field: 'operation', value: 'servicenow_read_record' }, + condition: { + field: 'operation', + value: ['servicenow_read_record', 'servicenow_aggregate'], + }, description: 'Return display values for reference fields instead of sys_ids', mode: 'advanced', }, @@ -215,6 +228,143 @@ Output: {"state": "2", "assigned_to": "john.doe", "work_notes": "Assigned and st condition: { field: 'operation', value: 'servicenow_delete_record' }, required: true, }, + // Aggregate-specific + { + id: 'count', + title: 'Return Count', + type: 'dropdown', + options: [ + { label: 'No', id: 'false' }, + { label: 'Yes', id: 'true' }, + ], + value: () => 'false', + condition: { field: 'operation', value: 'servicenow_aggregate' }, + description: 'Return the count of matching records', + }, + { + id: 'groupBy', + title: 'Group By', + type: 'short-input', + placeholder: 'category,priority', + condition: { field: 'operation', value: 'servicenow_aggregate' }, + description: 'Comma-separated fields to group results by', + wandConfig: { + enabled: true, + prompt: + 'Generate a comma-separated list of ServiceNow field names to group aggregate results by, based on the user request. Use lowercase field names. Return ONLY the comma-separated field names - no explanations, no extra text.', + placeholder: 'Describe how to group the results...', + generationType: 'custom', + }, + }, + { + id: 'avgFields', + title: 'Average Fields', + type: 'short-input', + placeholder: 'reassignment_count', + condition: { field: 'operation', value: 'servicenow_aggregate' }, + description: 'Comma-separated numeric fields to average', + mode: 'advanced', + }, + { + id: 'sumFields', + title: 'Sum Fields', + type: 'short-input', + placeholder: 'business_duration', + condition: { field: 'operation', value: 'servicenow_aggregate' }, + description: 'Comma-separated numeric fields to sum', + mode: 'advanced', + }, + { + id: 'minFields', + title: 'Min Fields', + type: 'short-input', + placeholder: 'opened_at', + condition: { field: 'operation', value: 'servicenow_aggregate' }, + description: 'Comma-separated fields to compute the minimum of', + mode: 'advanced', + }, + { + id: 'maxFields', + title: 'Max Fields', + type: 'short-input', + placeholder: 'closed_at', + condition: { field: 'operation', value: 'servicenow_aggregate' }, + description: 'Comma-separated fields to compute the maximum of', + mode: 'advanced', + }, + { + id: 'having', + title: 'Having', + type: 'short-input', + placeholder: 'count>5', + condition: { field: 'operation', value: 'servicenow_aggregate' }, + description: 'Filter on aggregate results', + mode: 'advanced', + }, + // Attachment record sys_id (list + upload) + { + id: 'recordSysId', + title: 'Record sys_id', + type: 'short-input', + placeholder: 'sys_id of the record', + condition: { + field: 'operation', + value: ['servicenow_list_attachments', 'servicenow_upload_attachment'], + }, + required: true, + description: 'sys_id of the record the attachment belongs to', + }, + // List attachments: limit + { + id: 'limit', + title: 'Limit', + type: 'short-input', + placeholder: '10', + condition: { field: 'operation', value: 'servicenow_list_attachments' }, + description: 'Maximum number of attachments to return', + mode: 'advanced', + }, + // Download attachment: attachment sys_id + { + id: 'attachmentSysId', + title: 'Attachment sys_id', + type: 'short-input', + placeholder: 'sys_id of the attachment', + condition: { field: 'operation', value: 'servicenow_download_attachment' }, + required: true, + description: 'sys_id of the attachment to download (from List Attachments)', + }, + // Upload attachment: file name + file + { + id: 'fileName', + title: 'File Name', + type: 'short-input', + placeholder: 'logs.txt', + condition: { field: 'operation', value: 'servicenow_upload_attachment' }, + required: true, + description: 'Name to give the uploaded file', + }, + { + id: 'uploadFile', + title: 'File', + type: 'file-upload', + canonicalParamId: 'file', + placeholder: 'Upload a file', + condition: { field: 'operation', value: 'servicenow_upload_attachment' }, + mode: 'basic', + multiple: false, + required: true, + }, + { + id: 'fileReference', + title: 'File', + type: 'short-input', + canonicalParamId: 'file', + placeholder: 'Reference a file from previous blocks (e.g., {{block_1.output.file}})', + condition: { field: 'operation', value: 'servicenow_upload_attachment' }, + mode: 'advanced', + required: true, + }, ...getTrigger('servicenow_incident_created').subBlocks, ...getTrigger('servicenow_incident_updated').subBlocks, ...getTrigger('servicenow_change_request_created').subBlocks, @@ -227,20 +377,36 @@ Output: {"state": "2", "assigned_to": "john.doe", "work_notes": "Assigned and st 'servicenow_read_record', 'servicenow_update_record', 'servicenow_delete_record', + 'servicenow_aggregate', + 'servicenow_list_attachments', + 'servicenow_download_attachment', + 'servicenow_upload_attachment', ], config: { tool: (params) => params.operation, params: (params) => { - const { operation, fields, ...rest } = params + const { operation, fields, file, ...rest } = params const isCreateOrUpdate = operation === 'servicenow_create_record' || operation === 'servicenow_update_record' if (rest.limit != null && rest.limit !== '') rest.limit = Number(rest.limit) if (rest.offset != null && rest.offset !== '') rest.offset = Number(rest.offset) - if (fields && isCreateOrUpdate) { - const parsedFields = typeof fields === 'string' ? JSON.parse(fields) : fields - return { ...rest, fields: parsedFields } + if (operation === 'servicenow_aggregate') { + rest.count = rest.count === true || rest.count === 'true' + } + + if (operation === 'servicenow_upload_attachment') { + const normalizedFile = normalizeFileInput(file, { single: true }) + return normalizedFile ? { ...rest, file: normalizedFile } : rest + } + + if (fields) { + if (isCreateOrUpdate) { + const parsedFields = typeof fields === 'string' ? JSON.parse(fields) : fields + return { ...rest, fields: parsedFields } + } + return { ...rest, fields } } return rest @@ -260,12 +426,32 @@ Output: {"state": "2", "assigned_to": "john.doe", "work_notes": "Assigned and st offset: { type: 'number', description: 'Pagination offset' }, fields: { type: 'json', description: 'Fields object or JSON string' }, displayValue: { type: 'string', description: 'Display value mode for reference fields' }, + count: { type: 'boolean', description: 'Return record count (aggregate)' }, + groupBy: { type: 'string', description: 'Comma-separated fields to group by (aggregate)' }, + avgFields: { type: 'string', description: 'Comma-separated fields to average (aggregate)' }, + sumFields: { type: 'string', description: 'Comma-separated fields to sum (aggregate)' }, + minFields: { type: 'string', description: 'Comma-separated fields to minimize (aggregate)' }, + maxFields: { type: 'string', description: 'Comma-separated fields to maximize (aggregate)' }, + having: { type: 'string', description: 'Aggregate result filter (aggregate)' }, + recordSysId: { type: 'string', description: 'Record sys_id for attachment operations' }, + attachmentSysId: { type: 'string', description: 'Attachment sys_id to download' }, + fileName: { type: 'string', description: 'Name of the file to upload' }, + file: { type: 'json', description: 'File to upload (canonical param)' }, }, outputs: { record: { type: 'json', description: 'Single ServiceNow record' }, records: { type: 'json', description: 'Array of ServiceNow records' }, success: { type: 'boolean', description: 'Operation success status' }, metadata: { type: 'json', description: 'Operation metadata' }, + result: { type: 'json', description: 'Aggregate result (stats or grouped array)' }, + count: { type: 'number', description: 'Aggregate matching record count' }, + attachments: { + type: 'json', + description: 'Attachment metadata list (sys_id, file_name, content_type, download_link)', + }, + file: { type: 'file', description: 'Downloaded attachment file' }, + content: { type: 'string', description: 'Base64-encoded downloaded file content' }, + attachment: { type: 'json', description: 'Uploaded attachment metadata' }, }, triggers: { enabled: true, diff --git a/apps/sim/blocks/blocks/workday.ts b/apps/sim/blocks/blocks/workday.ts index 5e1461d9b23..539cb060b25 100644 --- a/apps/sim/blocks/blocks/workday.ts +++ b/apps/sim/blocks/blocks/workday.ts @@ -433,12 +433,11 @@ Output: {"Marital_Status_Reference":{"ID":{"attributes":{"wd:type":"Marital_Stat worker: { type: 'json', description: - 'Worker profile (id, descriptor, primaryWorkEmail, primaryWorkPhone, businessTitle, supervisoryOrganization, hireDate, workerType, isActive)', + 'Worker profile (id, descriptor, personalData, employmentData, compensationData, organizationData)', }, workers: { type: 'json', - description: - 'Array of worker profiles (id, descriptor, primaryWorkEmail, businessTitle, supervisoryOrganization, hireDate, workerType, isActive)', + description: 'Array of worker profiles (id, descriptor, personalData, employmentData)', }, total: { type: 'number', description: 'Total count of results' }, preHireId: { type: 'string', description: 'Created pre-hire ID' }, diff --git a/apps/sim/blocks/blocks/zerobounce.ts b/apps/sim/blocks/blocks/zerobounce.ts index eb31615318e..73b1eeb274c 100644 --- a/apps/sim/blocks/blocks/zerobounce.ts +++ b/apps/sim/blocks/blocks/zerobounce.ts @@ -12,7 +12,7 @@ export const ZeroBounceBlock: BlockConfig = { docsLink: 'https://docs.sim.ai/tools/zerobounce', category: 'tools', integrationType: IntegrationType.Sales, - bgColor: '#330D49', + bgColor: '#FFFFFF', icon: ZeroBounceIcon, subBlocks: [ { diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index dc6f5ea5094..41732af3b29 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -4987,7 +4987,7 @@ export function IntercomIcon(props: SVGProps) { @@ -5028,7 +5028,7 @@ export function MailchimpIcon(props: SVGProps) { y='0px' viewBox='0 0 230.81 244.96' xmlSpace='preserve' - fill='currentColor' + fill='#000000' > diff --git a/apps/sim/lib/api/contracts/tools/index.ts b/apps/sim/lib/api/contracts/tools/index.ts index 9fb5e570b87..294eee7c473 100644 --- a/apps/sim/lib/api/contracts/tools/index.ts +++ b/apps/sim/lib/api/contracts/tools/index.ts @@ -21,6 +21,7 @@ export * from './pipedrive' export * from './quiver' export * from './sap' export * from './search' +export * from './servicenow' export * from './shared' export * from './stagehand' export * from './thinking' diff --git a/apps/sim/lib/api/contracts/tools/servicenow.ts b/apps/sim/lib/api/contracts/tools/servicenow.ts new file mode 100644 index 00000000000..cab00d7ae01 --- /dev/null +++ b/apps/sim/lib/api/contracts/tools/servicenow.ts @@ -0,0 +1,26 @@ +import { z } from 'zod' +import { defineRouteContract } from '@/lib/api/contracts/types' +import { RawFileInputSchema } from '@/lib/uploads/utils/file-schemas' + +export const servicenowUploadAttachmentBodySchema = z.object({ + instanceUrl: z.string().min(1, 'Instance URL is required'), + username: z.string().min(1, 'Username is required'), + password: z.string().min(1, 'Password is required'), + tableName: z.string().min(1, 'Table name is required'), + recordSysId: z.string().min(1, 'Record sys_id is required'), + fileName: z.string().min(1, 'File name is required'), + file: RawFileInputSchema.optional().nullable(), + fileContent: z.string().optional().nullable(), +}) + +export type ServiceNowUploadAttachmentBody = z.input + +// untyped-response: ServiceNow returns arbitrary attachment metadata wrapped in a success envelope +const servicenowToolResponseSchema = z.unknown() + +export const servicenowUploadAttachmentContract = defineRouteContract({ + method: 'POST', + path: '/api/tools/servicenow/upload-attachment', + body: servicenowUploadAttachmentBodySchema, + response: { mode: 'json', schema: servicenowToolResponseSchema }, +}) diff --git a/apps/sim/lib/integrations/integrations.json b/apps/sim/lib/integrations/integrations.json index 15a540768bb..dc45cec20f2 100644 --- a/apps/sim/lib/integrations/integrations.json +++ b/apps/sim/lib/integrations/integrations.json @@ -2173,7 +2173,7 @@ "name": "Bright Data", "description": "Scrape websites, search engines, and extract structured data", "longDescription": "Integrate Bright Data into the workflow. Scrape any URL with Web Unlocker, search Google and other engines with SERP API, discover web content ranked by intent, or trigger pre-built scrapers for structured data extraction.", - "bgColor": "#0F4C81", + "bgColor": "#FFFFFF", "iconName": "BrightDataIcon", "docsUrl": "https://docs.sim.ai/tools/brightdata", "operations": [ @@ -3370,10 +3370,22 @@ "name": "Execute SQL", "description": "Execute a SQL statement against a Databricks SQL warehouse and return results inline. Supports parameterized queries and Unity Catalog." }, + { + "name": "Get Statement", + "description": "Poll a SQL statement by its ID to retrieve status and results. Use this after Execute SQL when a query runs longer than the wait timeout." + }, + { + "name": "List Warehouses", + "description": "List all SQL warehouses in a Databricks workspace including their size, state, and type. Use this to discover the warehouse ID needed for Execute SQL." + }, { "name": "List Jobs", "description": "List all jobs in a Databricks workspace with optional filtering by name." }, + { + "name": "Get Job", + "description": "Get the full definition and settings of a single Databricks job by its job ID." + }, { "name": "Run Job", "description": "Trigger an existing Databricks job to run immediately with optional job-level or notebook parameters." @@ -3397,9 +3409,13 @@ { "name": "List Clusters", "description": "List all clusters in a Databricks workspace including their state, configuration, and resource details." + }, + { + "name": "Get Cluster", + "description": "Get the state, configuration, and resource details of a single Databricks cluster by its cluster ID." } ], - "operationCount": 8, + "operationCount": 12, "triggers": [], "triggerCount": 0, "authType": "api-key", @@ -3871,12 +3887,36 @@ "name": "List Links", "description": "Retrieve a paginated list of short links for the authenticated workspace. Supports filtering by domain, search query, tags, and sorting." }, + { + "name": "Count Links", + "description": "Retrieve the number of short links for the authenticated workspace, optionally filtered and grouped by domain, tag, user, or folder." + }, + { + "name": "Bulk Create Links", + "description": "Create up to 100 short links in a single request. Returns the created links alongside any per-link errors." + }, + { + "name": "Bulk Update Links", + "description": "Apply the same set of field updates to up to 100 links at once, selected by link IDs or external IDs." + }, + { + "name": "Bulk Delete Links", + "description": "Delete up to 100 short links in a single request by their link IDs. Non-existing IDs are ignored." + }, { "name": "Get Analytics", "description": "Retrieve analytics for links including clicks, leads, and sales. Supports filtering by link, time range, and grouping by various dimensions." + }, + { + "name": "List Events", + "description": "Retrieve a paginated stream of individual click, lead, and sale events for links, with filtering by link, time range, and location." + }, + { + "name": "Get QR Code", + "description": "Generate a customizable QR code (PNG) for a short link, with control over size, error correction, colors, and margin." } ], - "operationCount": 7, + "operationCount": 13, "triggers": [], "triggerCount": 0, "authType": "api-key", @@ -4170,6 +4210,10 @@ "name": "Sales Pointer (People)", "description": "Advanced people search with complex filters for location, company size, seniority, experience, and more." }, + { + "name": "Search Jobs", + "description": "Search LinkedIn job postings by keywords with filters for location, job type, workplace type, experience level, and company." + }, { "name": "Search Posts", "description": "Search LinkedIn posts by keywords with date filtering." @@ -4182,10 +4226,18 @@ "name": "Search Post Reactions", "description": "Get reactions on a LinkedIn post with filtering by reaction type." }, + { + "name": "Search Post Reactions (by URL)", + "description": "Get reactions on a LinkedIn post by its URL, filtered by reaction type." + }, { "name": "Search Post Comments", "description": "Get comments on a LinkedIn post." }, + { + "name": "Search Post Comments (by URL)", + "description": "Get comments on a LinkedIn post by its URL." + }, { "name": "Search People Activities", "description": "Get a person" @@ -4207,7 +4259,7 @@ "description": "Check your Enrich API credit usage and remaining balance." } ], - "operationCount": 29, + "operationCount": 32, "triggers": [], "triggerCount": 0, "authType": "api-key", @@ -5206,7 +5258,7 @@ }, { "name": "Get Extensive Calls", - "description": "Retrieve detailed call data including trackers, topics, and highlights from Gong." + "description": "Retrieve detailed call data including trackers, topics, highlights, and AI spotlight content (brief, outline, key points, call outcome) from Gong." }, { "name": "List Users", @@ -5220,6 +5272,14 @@ "name": "Aggregate Activity", "description": "Retrieve aggregated activity statistics for users by date range from Gong." }, + { + "name": "Day-by-Day Activity", + "description": "Retrieve detailed day-by-day activity (call IDs per activity type) for users by date range from Gong." + }, + { + "name": "Aggregate by Period", + "description": "Retrieve aggregated user activity grouped into time periods (day, week, month, quarter, year) by date range from Gong." + }, { "name": "Interaction Stats", "description": "Retrieve interaction statistics for users by date range from Gong. Only includes calls with Whisper enabled." @@ -5265,7 +5325,7 @@ "description": "Find all references to a phone number in Gong (calls, email messages, meetings, CRM data, and associated contacts)." } ], - "operationCount": 19, + "operationCount": 21, "triggers": [ { "id": "gong_webhook", @@ -7402,7 +7462,7 @@ "name": "Intercom", "description": "Manage contacts, companies, conversations, tickets, and messages in Intercom", "longDescription": "Integrate Intercom into the workflow. Can create, get, update, list, search, and delete contacts; create, get, and list companies; get, list, reply, and search conversations; create and get tickets; and create messages.", - "bgColor": "#1F8DED", + "bgColor": "#FFFFFF", "iconName": "IntercomIcon", "docsUrl": "https://docs.sim.ai/tools/intercom", "operations": [ @@ -9025,7 +9085,7 @@ "name": "Mailchimp", "description": "Manage audiences, campaigns, and marketing automation in Mailchimp", "longDescription": "Integrate Mailchimp into the workflow. Can manage audiences (lists), list members, campaigns, automation workflows, templates, reports, segments, tags, merge fields, interest categories, landing pages, signup forms, and batch operations.", - "bgColor": "#1A1A1A", + "bgColor": "#FFE01B", "iconName": "MailchimpIcon", "docsUrl": "https://docs.sim.ai/tools/mailchimp", "operations": [ @@ -9684,7 +9744,7 @@ "name": "MillionVerifier", "description": "Verify email deliverability and check account credits", "longDescription": "Integrate MillionVerifier to verify email deliverability in real time — classify addresses as valid, invalid, catch-all, disposable, or unknown — and check your remaining verification credits.", - "bgColor": "#00B67A", + "bgColor": "#FFFFFF", "iconName": "MillionVerifierIcon", "docsUrl": "https://docs.sim.ai/tools/millionverifier", "operations": [ @@ -13273,9 +13333,25 @@ { "name": "Delete Record", "description": "Delete a record from a ServiceNow table" + }, + { + "name": "Aggregate Records", + "description": "Compute aggregate statistics (count, sum, average, min, max, group by) over a ServiceNow table" + }, + { + "name": "List Attachments", + "description": "List the attachments on a ServiceNow record" + }, + { + "name": "Download Attachment", + "description": "Download an attachment file from ServiceNow by its sys_id" + }, + { + "name": "Upload Attachment", + "description": "Attach a file to a ServiceNow record" } ], - "operationCount": 4, + "operationCount": 8, "triggers": [ { "id": "servicenow_incident_created", @@ -13304,7 +13380,7 @@ } ], "triggerCount": 5, - "authType": "none", + "authType": "api-key", "category": "tools", "integrationType": "support", "tags": ["customer-support", "ticketing", "incident-management"] @@ -15591,7 +15667,7 @@ "name": "ZeroBounce", "description": "Validate email deliverability and check account credits", "longDescription": "Integrate ZeroBounce to validate email deliverability in real time — detect invalid, catch-all, spamtrap, abuse, and do-not-mail addresses — and check your remaining validation credits.", - "bgColor": "#330D49", + "bgColor": "#FFFFFF", "iconName": "ZeroBounceIcon", "docsUrl": "https://docs.sim.ai/tools/zerobounce", "operations": [ diff --git a/apps/sim/tools/databricks/cancel_run.ts b/apps/sim/tools/databricks/cancel_run.ts index 982ebc3dbd5..0b5ffa3f38a 100644 --- a/apps/sim/tools/databricks/cancel_run.ts +++ b/apps/sim/tools/databricks/cancel_run.ts @@ -34,7 +34,10 @@ export const cancelRunTool: ToolConfig { - const host = params.host.replace(/^https?:\/\//, '').replace(/\/$/, '') + const host = params.host + .trim() + .replace(/^https?:\/\//, '') + .replace(/\/$/, '') return `https://${host}/api/2.1/jobs/runs/cancel` }, method: 'POST', diff --git a/apps/sim/tools/databricks/execute_sql.ts b/apps/sim/tools/databricks/execute_sql.ts index 72d8f72b652..8564c2b3416 100644 --- a/apps/sim/tools/databricks/execute_sql.ts +++ b/apps/sim/tools/databricks/execute_sql.ts @@ -66,7 +66,10 @@ export const executeSqlTool: ToolConfig { - const host = params.host.replace(/^https?:\/\//, '').replace(/\/$/, '') + const host = params.host + .trim() + .replace(/^https?:\/\//, '') + .replace(/\/$/, '') return `https://${host}/api/2.0/sql/statements/` }, method: 'POST', diff --git a/apps/sim/tools/databricks/get_cluster.ts b/apps/sim/tools/databricks/get_cluster.ts new file mode 100644 index 00000000000..150b77f72ae --- /dev/null +++ b/apps/sim/tools/databricks/get_cluster.ts @@ -0,0 +1,137 @@ +import type { + DatabricksGetClusterParams, + DatabricksGetClusterResponse, +} from '@/tools/databricks/types' +import type { ToolConfig } from '@/tools/types' + +export const getClusterTool: ToolConfig = + { + id: 'databricks_get_cluster', + name: 'Databricks Get Cluster', + description: + 'Get the state, configuration, and resource details of a single Databricks cluster by its cluster ID.', + version: '1.0.0', + + params: { + host: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Databricks workspace host (e.g., dbc-abc123.cloud.databricks.com)', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Databricks Personal Access Token', + }, + clusterId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the cluster to retrieve', + }, + }, + + request: { + url: (params) => { + const host = params.host + .trim() + .replace(/^https?:\/\//, '') + .replace(/\/$/, '') + const url = new URL(`https://${host}/api/2.0/clusters/get`) + url.searchParams.set('cluster_id', params.clusterId.trim()) + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + Accept: 'application/json', + Authorization: `Bearer ${params.apiKey}`, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.message || data.error?.message || 'Failed to get cluster') + } + + return { + success: true, + output: { + cluster: { + clusterId: data.cluster_id ?? '', + clusterName: data.cluster_name ?? '', + state: data.state ?? 'UNKNOWN', + stateMessage: data.state_message ?? '', + creatorUserName: data.creator_user_name ?? '', + sparkVersion: data.spark_version ?? '', + nodeTypeId: data.node_type_id ?? '', + driverNodeTypeId: data.driver_node_type_id ?? '', + numWorkers: data.num_workers ?? null, + autoscale: data.autoscale + ? { + minWorkers: data.autoscale.min_workers ?? 0, + maxWorkers: data.autoscale.max_workers ?? 0, + } + : null, + clusterSource: data.cluster_source ?? '', + autoterminationMinutes: data.autotermination_minutes ?? 0, + startTime: data.start_time ?? null, + }, + }, + } + }, + + outputs: { + cluster: { + type: 'object', + description: 'Cluster detail', + properties: { + clusterId: { type: 'string', description: 'Unique cluster identifier' }, + clusterName: { type: 'string', description: 'Cluster display name' }, + state: { + type: 'string', + description: + 'Current state (PENDING, RUNNING, RESTARTING, RESIZING, TERMINATING, TERMINATED, ERROR, UNKNOWN)', + }, + stateMessage: { type: 'string', description: 'Human-readable state description' }, + creatorUserName: { type: 'string', description: 'Email of the cluster creator' }, + sparkVersion: { + type: 'string', + description: 'Spark runtime version (e.g., 13.3.x-scala2.12)', + }, + nodeTypeId: { type: 'string', description: 'Worker node type identifier' }, + driverNodeTypeId: { type: 'string', description: 'Driver node type identifier' }, + numWorkers: { + type: 'number', + description: 'Number of worker nodes (for fixed-size clusters)', + optional: true, + }, + autoscale: { + type: 'object', + description: 'Autoscaling configuration (null for fixed-size clusters)', + optional: true, + properties: { + minWorkers: { type: 'number', description: 'Minimum number of workers' }, + maxWorkers: { type: 'number', description: 'Maximum number of workers' }, + }, + }, + clusterSource: { + type: 'string', + description: 'Origin (API, UI, JOB, MODELS, PIPELINE, PIPELINE_MAINTENANCE, SQL)', + }, + autoterminationMinutes: { + type: 'number', + description: 'Minutes of inactivity before auto-termination (0 = disabled)', + }, + startTime: { + type: 'number', + description: 'Cluster start timestamp (epoch ms)', + optional: true, + }, + }, + }, + }, + } diff --git a/apps/sim/tools/databricks/get_job.ts b/apps/sim/tools/databricks/get_job.ts new file mode 100644 index 00000000000..e96cb48712e --- /dev/null +++ b/apps/sim/tools/databricks/get_job.ts @@ -0,0 +1,105 @@ +import type { DatabricksGetJobParams, DatabricksGetJobResponse } from '@/tools/databricks/types' +import type { ToolConfig } from '@/tools/types' + +export const getJobTool: ToolConfig = { + id: 'databricks_get_job', + name: 'Databricks Get Job', + description: 'Get the full definition and settings of a single Databricks job by its job ID.', + version: '1.0.0', + + params: { + host: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Databricks workspace host (e.g., dbc-abc123.cloud.databricks.com)', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Databricks Personal Access Token', + }, + jobId: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'The canonical identifier of the job to retrieve', + }, + }, + + request: { + url: (params) => { + const host = params.host + .trim() + .replace(/^https?:\/\//, '') + .replace(/\/$/, '') + const url = new URL(`https://${host}/api/2.1/jobs/get`) + url.searchParams.set('job_id', String(params.jobId)) + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + Accept: 'application/json', + Authorization: `Bearer ${params.apiKey}`, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.message || data.error?.message || 'Failed to get job') + } + + const settings = data.settings ?? {} + + return { + success: true, + output: { + jobId: data.job_id ?? 0, + name: settings.name ?? '', + creatorUserName: data.creator_user_name ?? '', + runAsUserName: data.run_as_user_name ?? '', + createdTime: data.created_time ?? 0, + format: settings.format ?? '', + maxConcurrentRuns: settings.max_concurrent_runs ?? 1, + timeoutSeconds: settings.timeout_seconds ?? null, + schedule: settings.schedule ?? null, + tags: settings.tags ?? null, + tasks: settings.tasks ?? [], + }, + } + }, + + outputs: { + jobId: { type: 'number', description: 'The job ID' }, + name: { type: 'string', description: 'Job name' }, + creatorUserName: { type: 'string', description: 'Email of the job creator' }, + runAsUserName: { type: 'string', description: 'User the job runs as' }, + createdTime: { type: 'number', description: 'Job creation timestamp (epoch ms)' }, + format: { type: 'string', description: 'Job format (SINGLE_TASK or MULTI_TASK)' }, + maxConcurrentRuns: { type: 'number', description: 'Maximum number of concurrent runs' }, + timeoutSeconds: { + type: 'number', + description: 'Job-level timeout in seconds (0 or null means no timeout)', + optional: true, + }, + schedule: { + type: 'object', + description: + 'Cron schedule configuration (quartz_cron_expression, timezone_id, pause_status)', + optional: true, + }, + tags: { + type: 'object', + description: 'Key-value tags applied to the job', + optional: true, + }, + tasks: { + type: 'array', + description: 'Task definitions for the job (empty for single-task jobs)', + items: { type: 'object' }, + }, + }, +} diff --git a/apps/sim/tools/databricks/get_run.ts b/apps/sim/tools/databricks/get_run.ts index de45ba1c6a9..f53190ffb4d 100644 --- a/apps/sim/tools/databricks/get_run.ts +++ b/apps/sim/tools/databricks/get_run.ts @@ -42,7 +42,10 @@ export const getRunTool: ToolConfig { - const host = params.host.replace(/^https?:\/\//, '').replace(/\/$/, '') + const host = params.host + .trim() + .replace(/^https?:\/\//, '') + .replace(/\/$/, '') const url = new URL(`https://${host}/api/2.1/jobs/runs/get`) url.searchParams.set('run_id', String(params.runId)) if (params.includeHistory) url.searchParams.set('include_history', 'true') diff --git a/apps/sim/tools/databricks/get_run_output.ts b/apps/sim/tools/databricks/get_run_output.ts index 79bb9f528d9..1fefd3da54e 100644 --- a/apps/sim/tools/databricks/get_run_output.ts +++ b/apps/sim/tools/databricks/get_run_output.ts @@ -37,7 +37,10 @@ export const getRunOutputTool: ToolConfig< request: { url: (params) => { - const host = params.host.replace(/^https?:\/\//, '').replace(/\/$/, '') + const host = params.host + .trim() + .replace(/^https?:\/\//, '') + .replace(/\/$/, '') return `https://${host}/api/2.1/jobs/runs/get-output?run_id=${params.runId}` }, method: 'GET', diff --git a/apps/sim/tools/databricks/get_statement.ts b/apps/sim/tools/databricks/get_statement.ts new file mode 100644 index 00000000000..ea32d4487a0 --- /dev/null +++ b/apps/sim/tools/databricks/get_statement.ts @@ -0,0 +1,136 @@ +import type { + DatabricksExecuteSqlResponse, + DatabricksGetStatementParams, +} from '@/tools/databricks/types' +import type { ToolConfig } from '@/tools/types' + +export const getStatementTool: ToolConfig< + DatabricksGetStatementParams, + DatabricksExecuteSqlResponse +> = { + id: 'databricks_get_statement', + name: 'Databricks Get Statement', + description: + 'Poll a SQL statement by its ID to retrieve status and results. Use this after Execute SQL when a query runs longer than the wait timeout.', + version: '1.0.0', + + params: { + host: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Databricks workspace host (e.g., dbc-abc123.cloud.databricks.com)', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Databricks Personal Access Token', + }, + statementId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the statement to fetch (returned by Execute SQL)', + }, + }, + + request: { + url: (params) => { + const host = params.host + .trim() + .replace(/^https?:\/\//, '') + .replace(/\/$/, '') + return `https://${host}/api/2.0/sql/statements/${params.statementId.trim()}` + }, + method: 'GET', + headers: (params) => ({ + Accept: 'application/json', + Authorization: `Bearer ${params.apiKey}`, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.message || data.error?.message || 'Failed to get statement') + } + + const status = data.status?.state ?? 'UNKNOWN' + if (status === 'FAILED') { + throw new Error( + data.status?.error?.message || + `SQL statement execution failed: ${data.status?.error?.error_code ?? 'UNKNOWN'}` + ) + } + + const columns = + data.manifest?.schema?.columns?.map( + (col: { name: string; position: number; type_name: string }) => ({ + name: col.name ?? '', + position: col.position ?? 0, + typeName: col.type_name ?? '', + }) + ) ?? null + + return { + success: true, + output: { + statementId: data.statement_id ?? '', + status, + columns, + data: data.result?.data_array ?? null, + totalRows: data.manifest?.total_row_count ?? null, + truncated: data.manifest?.truncated ?? false, + }, + } + }, + + outputs: { + statementId: { + type: 'string', + description: 'Unique identifier for the statement', + }, + status: { + type: 'string', + description: 'Execution status (SUCCEEDED, PENDING, RUNNING, FAILED, CANCELED, CLOSED)', + }, + columns: { + type: 'array', + description: 'Column schema of the result set', + optional: true, + items: { + type: 'object', + properties: { + name: { type: 'string', description: 'Column name' }, + position: { type: 'number', description: 'Column position (0-based)' }, + typeName: { + type: 'string', + description: + 'Column type (STRING, INT, LONG, DOUBLE, BOOLEAN, TIMESTAMP, DATE, DECIMAL, etc.)', + }, + }, + }, + }, + data: { + type: 'array', + description: + 'Result rows as a 2D array of strings where each inner array is a row of column values', + optional: true, + items: { + type: 'array', + description: 'A single row of column values as strings', + }, + }, + totalRows: { + type: 'number', + description: 'Total number of rows in the result', + optional: true, + }, + truncated: { + type: 'boolean', + description: 'Whether the result set was truncated due to row_limit or byte_limit', + }, + }, +} diff --git a/apps/sim/tools/databricks/index.ts b/apps/sim/tools/databricks/index.ts index 48c290d4559..5702c8023b6 100644 --- a/apps/sim/tools/databricks/index.ts +++ b/apps/sim/tools/databricks/index.ts @@ -1,17 +1,25 @@ import { cancelRunTool } from '@/tools/databricks/cancel_run' import { executeSqlTool } from '@/tools/databricks/execute_sql' +import { getClusterTool } from '@/tools/databricks/get_cluster' +import { getJobTool } from '@/tools/databricks/get_job' import { getRunTool } from '@/tools/databricks/get_run' import { getRunOutputTool } from '@/tools/databricks/get_run_output' +import { getStatementTool } from '@/tools/databricks/get_statement' import { listClustersTool } from '@/tools/databricks/list_clusters' import { listJobsTool } from '@/tools/databricks/list_jobs' import { listRunsTool } from '@/tools/databricks/list_runs' +import { listWarehousesTool } from '@/tools/databricks/list_warehouses' import { runJobTool } from '@/tools/databricks/run_job' export const databricksExecuteSqlTool = executeSqlTool +export const databricksGetStatementTool = getStatementTool export const databricksListJobsTool = listJobsTool +export const databricksGetJobTool = getJobTool export const databricksRunJobTool = runJobTool export const databricksGetRunTool = getRunTool export const databricksListRunsTool = listRunsTool export const databricksCancelRunTool = cancelRunTool export const databricksGetRunOutputTool = getRunOutputTool export const databricksListClustersTool = listClustersTool +export const databricksGetClusterTool = getClusterTool +export const databricksListWarehousesTool = listWarehousesTool diff --git a/apps/sim/tools/databricks/list_clusters.ts b/apps/sim/tools/databricks/list_clusters.ts index c06802a727b..cc2e31491b3 100644 --- a/apps/sim/tools/databricks/list_clusters.ts +++ b/apps/sim/tools/databricks/list_clusters.ts @@ -25,7 +25,10 @@ export const listClustersTool: ToolConfig { - const host = params.host.replace(/^https?:\/\//, '').replace(/\/$/, '') + const host = params.host + .trim() + .replace(/^https?:\/\//, '') + .replace(/\/$/, '') return `https://${host}/api/2.0/clusters/list` }, method: 'GET', diff --git a/apps/sim/tools/databricks/list_jobs.ts b/apps/sim/tools/databricks/list_jobs.ts index 7d347770c42..194493c7ed7 100644 --- a/apps/sim/tools/databricks/list_jobs.ts +++ b/apps/sim/tools/databricks/list_jobs.ts @@ -48,7 +48,10 @@ export const listJobsTool: ToolConfig { - const host = params.host.replace(/^https?:\/\//, '').replace(/\/$/, '') + const host = params.host + .trim() + .replace(/^https?:\/\//, '') + .replace(/\/$/, '') const url = new URL(`https://${host}/api/2.1/jobs/list`) if (params.limit) url.searchParams.set('limit', String(params.limit)) if (params.offset) url.searchParams.set('offset', String(params.offset)) diff --git a/apps/sim/tools/databricks/list_runs.ts b/apps/sim/tools/databricks/list_runs.ts index 3e7b65bab64..69dc333f023 100644 --- a/apps/sim/tools/databricks/list_runs.ts +++ b/apps/sim/tools/databricks/list_runs.ts @@ -73,7 +73,10 @@ export const listRunsTool: ToolConfig { - const host = params.host.replace(/^https?:\/\//, '').replace(/\/$/, '') + const host = params.host + .trim() + .replace(/^https?:\/\//, '') + .replace(/\/$/, '') const url = new URL(`https://${host}/api/2.1/jobs/runs/list`) if (params.jobId) url.searchParams.set('job_id', String(params.jobId)) if (params.activeOnly) url.searchParams.set('active_only', 'true') diff --git a/apps/sim/tools/databricks/list_warehouses.ts b/apps/sim/tools/databricks/list_warehouses.ts new file mode 100644 index 00000000000..dcb716360ef --- /dev/null +++ b/apps/sim/tools/databricks/list_warehouses.ts @@ -0,0 +1,127 @@ +import type { + DatabricksBaseParams, + DatabricksListWarehousesResponse, +} from '@/tools/databricks/types' +import type { ToolConfig } from '@/tools/types' + +export const listWarehousesTool: ToolConfig< + DatabricksBaseParams, + DatabricksListWarehousesResponse +> = { + id: 'databricks_list_warehouses', + name: 'Databricks List Warehouses', + description: + 'List all SQL warehouses in a Databricks workspace including their size, state, and type. Use this to discover the warehouse ID needed for Execute SQL.', + version: '1.0.0', + + params: { + host: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Databricks workspace host (e.g., dbc-abc123.cloud.databricks.com)', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Databricks Personal Access Token', + }, + }, + + request: { + url: (params) => { + const host = params.host + .trim() + .replace(/^https?:\/\//, '') + .replace(/\/$/, '') + return `https://${host}/api/2.0/sql/warehouses` + }, + method: 'GET', + headers: (params) => ({ + Accept: 'application/json', + Authorization: `Bearer ${params.apiKey}`, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.message || data.error?.message || 'Failed to list warehouses') + } + + const warehouses = (data.warehouses ?? []).map( + (warehouse: { + id?: string + name?: string + cluster_size?: string + state?: string + warehouse_type?: string + creator_name?: string + auto_stop_mins?: number + num_clusters?: number + min_num_clusters?: number + max_num_clusters?: number + num_active_sessions?: number + enable_serverless_compute?: boolean + }) => ({ + warehouseId: warehouse.id ?? '', + name: warehouse.name ?? '', + clusterSize: warehouse.cluster_size ?? '', + state: warehouse.state ?? 'UNKNOWN', + warehouseType: warehouse.warehouse_type ?? '', + creatorName: warehouse.creator_name ?? '', + autoStopMinutes: warehouse.auto_stop_mins ?? 0, + numClusters: warehouse.num_clusters ?? 0, + minNumClusters: warehouse.min_num_clusters ?? 0, + maxNumClusters: warehouse.max_num_clusters ?? 0, + numActiveSessions: warehouse.num_active_sessions ?? 0, + enableServerlessCompute: warehouse.enable_serverless_compute ?? false, + }) + ) + + return { + success: true, + output: { + warehouses, + }, + } + }, + + outputs: { + warehouses: { + type: 'array', + description: 'List of SQL warehouses in the workspace', + items: { + type: 'object', + properties: { + warehouseId: { type: 'string', description: 'Unique warehouse identifier' }, + name: { type: 'string', description: 'Warehouse display name' }, + clusterSize: { + type: 'string', + description: 'Warehouse size (e.g., 2X-Small, Small, Medium, Large)', + }, + state: { + type: 'string', + description: 'Current state (STARTING, RUNNING, STOPPING, STOPPED, DELETING, DELETED)', + }, + warehouseType: { type: 'string', description: 'Warehouse type (CLASSIC, PRO)' }, + creatorName: { type: 'string', description: 'Email of the warehouse creator' }, + autoStopMinutes: { + type: 'number', + description: 'Minutes of inactivity before auto-stop (0 = disabled)', + }, + numClusters: { type: 'number', description: 'Current number of running clusters' }, + minNumClusters: { type: 'number', description: 'Minimum cluster count for scaling' }, + maxNumClusters: { type: 'number', description: 'Maximum cluster count for scaling' }, + numActiveSessions: { type: 'number', description: 'Number of active sessions' }, + enableServerlessCompute: { + type: 'boolean', + description: 'Whether serverless compute is enabled', + }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/databricks/run_job.ts b/apps/sim/tools/databricks/run_job.ts index d4739488603..a7d93a7db11 100644 --- a/apps/sim/tools/databricks/run_job.ts +++ b/apps/sim/tools/databricks/run_job.ts @@ -50,7 +50,10 @@ export const runJobTool: ToolConfig { - const host = params.host.replace(/^https?:\/\//, '').replace(/\/$/, '') + const host = params.host + .trim() + .replace(/^https?:\/\//, '') + .replace(/\/$/, '') return `https://${host}/api/2.1/jobs/run-now` }, method: 'POST', diff --git a/apps/sim/tools/databricks/types.ts b/apps/sim/tools/databricks/types.ts index 362fc21acb6..f5b4bc47c61 100644 --- a/apps/sim/tools/databricks/types.ts +++ b/apps/sim/tools/databricks/types.ts @@ -27,6 +27,11 @@ export interface DatabricksExecuteSqlResponse extends ToolResponse { } } +/** Get Statement (poll an async SQL statement by its ID) */ +export interface DatabricksGetStatementParams extends DatabricksBaseParams { + statementId: string +} + /** List Jobs */ export interface DatabricksListJobsParams extends DatabricksBaseParams { limit?: number @@ -65,6 +70,27 @@ export interface DatabricksRunJobResponse extends ToolResponse { } } +/** Get Job */ +export interface DatabricksGetJobParams extends DatabricksBaseParams { + jobId: number +} + +export interface DatabricksGetJobResponse extends ToolResponse { + output: { + jobId: number + name: string + creatorUserName: string + runAsUserName: string + createdTime: number + format: string + maxConcurrentRuns: number + timeoutSeconds: number | null + schedule: Record | null + tags: Record | null + tasks: Array> + } +} + /** Get Run */ export interface DatabricksGetRunParams extends DatabricksBaseParams { runId: number @@ -158,23 +184,57 @@ export interface DatabricksGetRunOutputResponse extends ToolResponse { } } +/** Shared cluster shape returned by list_clusters and get_cluster */ +export interface DatabricksCluster { + clusterId: string + clusterName: string + state: string + stateMessage: string + creatorUserName: string + sparkVersion: string + nodeTypeId: string + driverNodeTypeId: string + numWorkers: number | null + autoscale: { minWorkers: number; maxWorkers: number } | null + clusterSource: string + autoterminationMinutes: number + startTime: number | null +} + /** List Clusters */ export interface DatabricksListClustersResponse extends ToolResponse { output: { - clusters: Array<{ - clusterId: string - clusterName: string + clusters: DatabricksCluster[] + } +} + +/** Get Cluster */ +export interface DatabricksGetClusterParams extends DatabricksBaseParams { + clusterId: string +} + +export interface DatabricksGetClusterResponse extends ToolResponse { + output: { + cluster: DatabricksCluster + } +} + +/** List Warehouses */ +export interface DatabricksListWarehousesResponse extends ToolResponse { + output: { + warehouses: Array<{ + warehouseId: string + name: string + clusterSize: string state: string - stateMessage: string - creatorUserName: string - sparkVersion: string - nodeTypeId: string - driverNodeTypeId: string - numWorkers: number | null - autoscale: { minWorkers: number; maxWorkers: number } | null - clusterSource: string - autoterminationMinutes: number - startTime: number | null + warehouseType: string + creatorName: string + autoStopMinutes: number + numClusters: number + minNumClusters: number + maxNumClusters: number + numActiveSessions: number + enableServerlessCompute: boolean }> } } @@ -184,8 +244,11 @@ export type DatabricksResponse = | DatabricksExecuteSqlResponse | DatabricksListJobsResponse | DatabricksRunJobResponse + | DatabricksGetJobResponse | DatabricksGetRunResponse | DatabricksListRunsResponse | DatabricksCancelRunResponse | DatabricksGetRunOutputResponse | DatabricksListClustersResponse + | DatabricksGetClusterResponse + | DatabricksListWarehousesResponse diff --git a/apps/sim/tools/dub/bulk_create_links.ts b/apps/sim/tools/dub/bulk_create_links.ts new file mode 100644 index 00000000000..53356c08446 --- /dev/null +++ b/apps/sim/tools/dub/bulk_create_links.ts @@ -0,0 +1,73 @@ +import type { DubBulkCreateLinksParams, DubBulkCreateLinksResponse } from '@/tools/dub/types' +import type { ToolConfig } from '@/tools/types' + +export const bulkCreateLinksTool: ToolConfig = + { + id: 'dub_bulk_create_links', + name: 'Dub Bulk Create Links', + description: + 'Create up to 100 short links in a single request. Returns the created links alongside any per-link errors.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Dub API key', + }, + links: { + type: 'json', + required: true, + visibility: 'user-or-llm', + description: + 'JSON array of link objects to create. Each object requires a "url" and may include domain, key, tagIds, and other link fields (max 100).', + }, + }, + + request: { + url: 'https://api.dub.co/links/bulk', + method: 'POST', + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.apiKey}`, + }), + body: (params) => { + const links = typeof params.links === 'string' ? JSON.parse(params.links) : params.links + return Array.isArray(links) ? links : [links] + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error?.message || data.error || 'Failed to bulk create links') + } + + const results = Array.isArray(data) ? (data as Record[]) : [] + const created = results.filter((item) => !item.error) + const errors = results.filter((item) => item.error) + + return { + success: true, + output: { + created, + errors, + count: created.length, + }, + } + }, + + outputs: { + created: { + type: 'json', + description: 'Array of successfully created link objects', + }, + errors: { + type: 'json', + description: 'Array of per-link errors ({ link, error, code }) for links that failed', + }, + count: { type: 'number', description: 'Number of links successfully created' }, + }, + } diff --git a/apps/sim/tools/dub/bulk_delete_links.ts b/apps/sim/tools/dub/bulk_delete_links.ts new file mode 100644 index 00000000000..8ebfc172bb2 --- /dev/null +++ b/apps/sim/tools/dub/bulk_delete_links.ts @@ -0,0 +1,62 @@ +import type { DubBulkDeleteLinksParams, DubBulkDeleteLinksResponse } from '@/tools/dub/types' +import type { ToolConfig } from '@/tools/types' + +export const bulkDeleteLinksTool: ToolConfig = + { + id: 'dub_bulk_delete_links', + name: 'Dub Bulk Delete Links', + description: + 'Delete up to 100 short links in a single request by their link IDs. Non-existing IDs are ignored.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Dub API key', + }, + linkIds: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Comma-separated link IDs to delete (max 100)', + }, + }, + + request: { + url: (params) => { + const url = new URL('https://api.dub.co/links/bulk') + const ids = params.linkIds + .split(',') + .map((id) => id.trim()) + .filter(Boolean) + url.searchParams.set('linkIds', ids.join(',')) + return url.toString() + }, + method: 'DELETE', + headers: (params) => ({ + Accept: 'application/json', + Authorization: `Bearer ${params.apiKey}`, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error?.message || data.error || 'Failed to bulk delete links') + } + + return { + success: true, + output: { + deletedCount: data.deletedCount ?? 0, + }, + } + }, + + outputs: { + deletedCount: { type: 'number', description: 'Number of links that were deleted' }, + }, + } diff --git a/apps/sim/tools/dub/bulk_update_links.ts b/apps/sim/tools/dub/bulk_update_links.ts new file mode 100644 index 00000000000..bab05c3f052 --- /dev/null +++ b/apps/sim/tools/dub/bulk_update_links.ts @@ -0,0 +1,85 @@ +import type { DubBulkUpdateLinksParams, DubBulkUpdateLinksResponse } from '@/tools/dub/types' +import type { ToolConfig } from '@/tools/types' + +export const bulkUpdateLinksTool: ToolConfig = + { + id: 'dub_bulk_update_links', + name: 'Dub Bulk Update Links', + description: + 'Apply the same set of field updates to up to 100 links at once, selected by link IDs or external IDs.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Dub API key', + }, + linkIds: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Comma-separated link IDs to update (max 100, takes precedence over externalIds)', + }, + externalIds: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Comma-separated external IDs to update (max 100)', + }, + data: { + type: 'json', + required: true, + visibility: 'user-or-llm', + description: + 'JSON object of fields to apply to every selected link (e.g. { "archived": true, "tagIds": ["..."] })', + }, + }, + + request: { + url: 'https://api.dub.co/links/bulk', + method: 'PATCH', + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.apiKey}`, + }), + body: (params) => { + const data = typeof params.data === 'string' ? JSON.parse(params.data) : params.data + const body: Record = { data: data ?? {} } + if (params.linkIds) { + body.linkIds = params.linkIds.split(',').map((id) => id.trim()) + } else if (params.externalIds) { + body.externalIds = params.externalIds.split(',').map((id) => id.trim()) + } + return body + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error?.message || data.error || 'Failed to bulk update links') + } + + const updated = Array.isArray(data) ? (data as Record[]) : [] + + return { + success: true, + output: { + updated, + count: updated.length, + }, + } + }, + + outputs: { + updated: { + type: 'json', + description: 'Array of updated link objects', + }, + count: { type: 'number', description: 'Number of links updated' }, + }, + } diff --git a/apps/sim/tools/dub/get_events.ts b/apps/sim/tools/dub/get_events.ts new file mode 100644 index 00000000000..2bf280ad321 --- /dev/null +++ b/apps/sim/tools/dub/get_events.ts @@ -0,0 +1,142 @@ +import type { DubGetEventsParams, DubGetEventsResponse } from '@/tools/dub/types' +import type { ToolConfig } from '@/tools/types' + +export const getEventsTool: ToolConfig = { + id: 'dub_get_events', + name: 'Dub List Events', + description: + 'Retrieve a paginated stream of individual click, lead, and sale events for links, with filtering by link, time range, and location.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Dub API key', + }, + event: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Event type: clicks (default), leads, or sales', + }, + linkId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter by link ID', + }, + externalId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter by external ID (prefix with ext_)', + }, + domain: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter by domain', + }, + interval: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Time interval: 24h (default), 7d, 30d, 90d, 1y, mtd, qtd, ytd, or all', + }, + start: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Start date/time in ISO 8601 format (overrides interval)', + }, + end: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'End date/time in ISO 8601 format (defaults to now)', + }, + country: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter by country (ISO 3166-1 alpha-2 code)', + }, + timezone: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'IANA timezone for event timestamps (defaults to UTC)', + }, + page: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Page number (default: 1)', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of events per page (default: 100, max: 1000)', + }, + sortOrder: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Sort order: desc (default) or asc', + }, + }, + + request: { + url: (params) => { + const url = new URL('https://api.dub.co/events') + if (params.event) url.searchParams.set('event', params.event) + if (params.linkId) url.searchParams.set('linkId', params.linkId) + if (params.externalId) url.searchParams.set('externalId', params.externalId) + if (params.domain) url.searchParams.set('domain', params.domain) + if (params.interval) url.searchParams.set('interval', params.interval) + if (params.start) url.searchParams.set('start', params.start) + if (params.end) url.searchParams.set('end', params.end) + if (params.country) url.searchParams.set('country', params.country) + if (params.timezone) url.searchParams.set('timezone', params.timezone) + if (params.page) url.searchParams.set('page', String(params.page)) + if (params.limit) url.searchParams.set('limit', String(params.limit)) + if (params.sortOrder) url.searchParams.set('sortOrder', params.sortOrder) + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + Accept: 'application/json', + Authorization: `Bearer ${params.apiKey}`, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error?.message || data.error || 'Failed to list events') + } + + const events = Array.isArray(data) ? (data as Record[]) : [] + + return { + success: true, + output: { + events, + count: events.length, + }, + } + }, + + outputs: { + events: { + type: 'json', + description: + 'Array of event objects (event, timestamp, click, link, and customer/sale data when applicable)', + }, + count: { type: 'number', description: 'Number of events returned' }, + }, +} diff --git a/apps/sim/tools/dub/get_links_count.ts b/apps/sim/tools/dub/get_links_count.ts new file mode 100644 index 00000000000..2d16ed16ef3 --- /dev/null +++ b/apps/sim/tools/dub/get_links_count.ts @@ -0,0 +1,119 @@ +import type { DubGetLinksCountParams, DubGetLinksCountResponse } from '@/tools/dub/types' +import type { ToolConfig } from '@/tools/types' + +export const getLinksCountTool: ToolConfig = { + id: 'dub_get_links_count', + name: 'Dub Count Links', + description: + 'Retrieve the number of short links for the authenticated workspace, optionally filtered and grouped by domain, tag, user, or folder.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Dub API key', + }, + domain: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter by domain', + }, + search: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Search query matched against the short link slug and destination URL', + }, + tagIds: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Comma-separated tag IDs to filter by', + }, + tagNames: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Comma-separated tag names to filter by (case-insensitive)', + }, + folderId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter by folder ID', + }, + showArchived: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether to include archived links (defaults to false)', + }, + groupBy: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Group counts by: domain, tagId, userId, or folderId', + }, + }, + + request: { + url: (params) => { + const url = new URL('https://api.dub.co/links/count') + if (params.domain) url.searchParams.set('domain', params.domain) + if (params.search) url.searchParams.set('search', params.search) + if (params.tagIds) url.searchParams.set('tagIds', params.tagIds) + if (params.tagNames) url.searchParams.set('tagNames', params.tagNames) + if (params.folderId) url.searchParams.set('folderId', params.folderId) + if (params.showArchived !== undefined) + url.searchParams.set('showArchived', String(params.showArchived)) + if (params.groupBy) url.searchParams.set('groupBy', params.groupBy) + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + Accept: 'application/json', + Authorization: `Bearer ${params.apiKey}`, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error?.message || data.error || 'Failed to count links') + } + + if (typeof data === 'number') { + return { + success: true, + output: { + count: data, + groups: null, + }, + } + } + + const groups = Array.isArray(data) ? (data as Record[]) : [] + const count = groups.reduce((sum, group) => sum + (Number(group.count) || 0), 0) + + return { + success: true, + output: { + count, + groups, + }, + } + }, + + outputs: { + count: { type: 'number', description: 'Total number of links matching the filters' }, + groups: { + type: 'json', + description: 'Per-group counts when groupBy is set (e.g. [{ domain, count }])', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/dub/get_qr_code.ts b/apps/sim/tools/dub/get_qr_code.ts new file mode 100644 index 00000000000..6502d66b1f2 --- /dev/null +++ b/apps/sim/tools/dub/get_qr_code.ts @@ -0,0 +1,122 @@ +import type { DubGetQrCodeParams, DubGetQrCodeResponse } from '@/tools/dub/types' +import type { ToolConfig } from '@/tools/types' + +export const getQrCodeTool: ToolConfig = { + id: 'dub_get_qr_code', + name: 'Dub Get QR Code', + description: + 'Generate a customizable QR code (PNG) for a short link, with control over size, error correction, colors, and margin.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Dub API key', + }, + url: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The short link URL to encode in the QR code', + }, + size: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'QR code size in pixels (default: 600)', + }, + level: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Error correction level: L (default), M, Q, or H', + }, + fgColor: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Foreground color in hex (default: #000000)', + }, + bgColor: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Background color in hex (default: #FFFFFF)', + }, + hideLogo: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether to hide the logo in the center of the QR code (default: false)', + }, + margin: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Margin (quiet zone) around the QR code (default: 2)', + }, + }, + + request: { + url: (params) => { + const url = new URL('https://api.dub.co/qr') + url.searchParams.set('url', params.url.trim()) + if (params.size !== undefined) url.searchParams.set('size', String(params.size)) + if (params.level) url.searchParams.set('level', params.level) + if (params.fgColor) url.searchParams.set('fgColor', params.fgColor) + if (params.bgColor) url.searchParams.set('bgColor', params.bgColor) + if (params.hideLogo !== undefined) url.searchParams.set('hideLogo', String(params.hideLogo)) + if (params.margin !== undefined) url.searchParams.set('margin', String(params.margin)) + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + Accept: 'image/png', + Authorization: `Bearer ${params.apiKey}`, + }), + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const errorText = await response.text() + let message = errorText || `Failed to generate QR code: ${response.status}` + try { + const parsed = JSON.parse(errorText) + message = parsed.error?.message || parsed.error || message + } catch { + // Non-JSON error body; use the raw text + } + throw new Error(message) + } + + const arrayBuffer = await response.arrayBuffer() + const buffer = Buffer.from(arrayBuffer) + const mimeType = response.headers.get('content-type') || 'image/png' + + return { + success: true, + output: { + file: { + name: 'qrcode.png', + mimeType, + data: buffer.toString('base64'), + size: buffer.length, + }, + content: buffer.toString('base64'), + }, + } + }, + + outputs: { + file: { + type: 'file', + description: 'Generated QR code image stored in execution files', + }, + content: { + type: 'string', + description: 'Base64-encoded PNG image data', + }, + }, +} diff --git a/apps/sim/tools/dub/index.ts b/apps/sim/tools/dub/index.ts index 07d5bd5b9f8..401d48c5f03 100644 --- a/apps/sim/tools/dub/index.ts +++ b/apps/sim/tools/dub/index.ts @@ -1,7 +1,13 @@ +import { bulkCreateLinksTool } from '@/tools/dub/bulk_create_links' +import { bulkDeleteLinksTool } from '@/tools/dub/bulk_delete_links' +import { bulkUpdateLinksTool } from '@/tools/dub/bulk_update_links' import { createLinkTool } from '@/tools/dub/create_link' import { deleteLinkTool } from '@/tools/dub/delete_link' import { getAnalyticsTool } from '@/tools/dub/get_analytics' +import { getEventsTool } from '@/tools/dub/get_events' import { getLinkTool } from '@/tools/dub/get_link' +import { getLinksCountTool } from '@/tools/dub/get_links_count' +import { getQrCodeTool } from '@/tools/dub/get_qr_code' import { listLinksTool } from '@/tools/dub/list_links' import { updateLinkTool } from '@/tools/dub/update_link' import { upsertLinkTool } from '@/tools/dub/upsert_link' @@ -13,3 +19,9 @@ export const dubUpsertLinkTool = upsertLinkTool export const dubDeleteLinkTool = deleteLinkTool export const dubListLinksTool = listLinksTool export const dubGetAnalyticsTool = getAnalyticsTool +export const dubGetLinksCountTool = getLinksCountTool +export const dubGetEventsTool = getEventsTool +export const dubBulkCreateLinksTool = bulkCreateLinksTool +export const dubBulkUpdateLinksTool = bulkUpdateLinksTool +export const dubBulkDeleteLinksTool = bulkDeleteLinksTool +export const dubGetQrCodeTool = getQrCodeTool diff --git a/apps/sim/tools/dub/list_links.ts b/apps/sim/tools/dub/list_links.ts index e1ec8ad037d..c5d478ebb0c 100644 --- a/apps/sim/tools/dub/list_links.ts +++ b/apps/sim/tools/dub/list_links.ts @@ -39,18 +39,6 @@ export const listLinksTool: ToolConfig visibility: 'user-or-llm', description: 'Whether to include archived links (defaults to false)', }, - sortBy: { - type: 'string', - required: false, - visibility: 'user-or-llm', - description: 'Sort by field: createdAt, clicks, saleAmount, or lastClicked', - }, - sortOrder: { - type: 'string', - required: false, - visibility: 'user-or-llm', - description: 'Sort order: asc or desc', - }, page: { type: 'number', required: false, @@ -73,8 +61,6 @@ export const listLinksTool: ToolConfig if (params.tagIds) url.searchParams.set('tagIds', params.tagIds) if (params.showArchived !== undefined) url.searchParams.set('showArchived', String(params.showArchived)) - if (params.sortBy) url.searchParams.set('sortBy', params.sortBy) - if (params.sortOrder) url.searchParams.set('sortOrder', params.sortOrder) if (params.page) url.searchParams.set('page', String(params.page)) if (params.pageSize) url.searchParams.set('pageSize', String(params.pageSize)) return url.toString() diff --git a/apps/sim/tools/dub/types.ts b/apps/sim/tools/dub/types.ts index 3126ba0e43e..53488e74c1d 100644 --- a/apps/sim/tools/dub/types.ts +++ b/apps/sim/tools/dub/types.ts @@ -81,8 +81,6 @@ export interface DubListLinksParams extends DubBaseParams { search?: string tagIds?: string showArchived?: boolean - sortBy?: string - sortOrder?: string page?: number pageSize?: number } @@ -100,6 +98,55 @@ export interface DubGetAnalyticsParams extends DubBaseParams { timezone?: string } +export interface DubGetLinksCountParams extends DubBaseParams { + domain?: string + search?: string + tagIds?: string + tagNames?: string + folderId?: string + showArchived?: boolean + groupBy?: string +} + +export interface DubGetEventsParams extends DubBaseParams { + event?: string + linkId?: string + externalId?: string + domain?: string + interval?: string + start?: string + end?: string + country?: string + timezone?: string + page?: number + limit?: number + sortOrder?: string +} + +export interface DubBulkCreateLinksParams extends DubBaseParams { + links: unknown +} + +export interface DubBulkUpdateLinksParams extends DubBaseParams { + linkIds?: string + externalIds?: string + data: unknown +} + +export interface DubBulkDeleteLinksParams extends DubBaseParams { + linkIds: string +} + +export interface DubGetQrCodeParams extends DubBaseParams { + url: string + size?: number + level?: string + fgColor?: string + bgColor?: string + hideLogo?: boolean + margin?: number +} + interface DubLink { id: string domain: string @@ -165,6 +212,53 @@ export interface DubGetAnalyticsResponse extends ToolResponse { } } +export interface DubGetLinksCountResponse extends ToolResponse { + output: { + count: number + groups: Record[] | null + } +} + +export interface DubGetEventsResponse extends ToolResponse { + output: { + events: Record[] + count: number + } +} + +export interface DubBulkCreateLinksResponse extends ToolResponse { + output: { + created: Record[] + errors: Record[] + count: number + } +} + +export interface DubBulkUpdateLinksResponse extends ToolResponse { + output: { + updated: Record[] + count: number + } +} + +export interface DubBulkDeleteLinksResponse extends ToolResponse { + output: { + deletedCount: number + } +} + +export interface DubGetQrCodeResponse extends ToolResponse { + output: { + file: { + name: string + mimeType: string + data: string + size: number + } + content: string + } +} + export type DubResponse = | DubCreateLinkResponse | DubGetLinkResponse @@ -173,3 +267,9 @@ export type DubResponse = | DubDeleteLinkResponse | DubListLinksResponse | DubGetAnalyticsResponse + | DubGetLinksCountResponse + | DubGetEventsResponse + | DubBulkCreateLinksResponse + | DubBulkUpdateLinksResponse + | DubBulkDeleteLinksResponse + | DubGetQrCodeResponse diff --git a/apps/sim/tools/duckduckgo/search.ts b/apps/sim/tools/duckduckgo/search.ts index 3e61a4144f7..bd34787cf80 100644 --- a/apps/sim/tools/duckduckgo/search.ts +++ b/apps/sim/tools/duckduckgo/search.ts @@ -49,8 +49,7 @@ export const searchTool: ToolConfig { const data = await response.json() - // Map related topics - const relatedTopics = (data.RelatedTopics || []).map((topic: any) => ({ + const mapTopic = (topic: any) => ({ FirstURL: topic.FirstURL, Text: topic.Text, Result: topic.Result, @@ -61,7 +60,16 @@ export const searchTool: ToolConfig + Array.isArray(topic.Topics) ? topic.Topics.map(mapTopic) : [mapTopic(topic)] + ) // Map results (external links) const results = (data.Results || []).map((result: any) => ({ @@ -85,10 +93,14 @@ export const searchTool: ToolConfig { - const url = new URL('https://api.enrich.so/v2/api/linkedin-to-email') - url.searchParams.append('linkedin_profile', params.linkedinProfile.trim()) + const url = new URL('https://api.enrich.so/v1/api/find-personal-email') + url.searchParams.append('profile_url', params.linkedinProfile.trim()) return url.toString() }, method: 'GET', @@ -44,6 +44,19 @@ export const linkedInToPersonalEmailTool: ToolConfig< transformResponse: async (response: Response) => { const data = await response.json() + + // Handle queued response (202) + if (data.status === 'in_progress' || data.message?.includes('queued')) { + return { + success: true, + output: { + email: null, + found: false, + status: 'in_progress', + }, + } + } + const resultData = data.data ?? data return { @@ -51,7 +64,7 @@ export const linkedInToPersonalEmailTool: ToolConfig< output: { email: resultData.email ?? resultData.personal_email ?? null, found: resultData.found ?? Boolean(resultData.email ?? resultData.personal_email), - status: resultData.status ?? null, + status: resultData.status ?? 'completed', }, } }, diff --git a/apps/sim/tools/enrich/search_jobs.ts b/apps/sim/tools/enrich/search_jobs.ts new file mode 100644 index 00000000000..dc02421bb66 --- /dev/null +++ b/apps/sim/tools/enrich/search_jobs.ts @@ -0,0 +1,149 @@ +import type { EnrichSearchJobsParams, EnrichSearchJobsResponse } from '@/tools/enrich/types' +import type { ToolConfig } from '@/tools/types' + +export const searchJobsTool: ToolConfig = { + id: 'enrich_search_jobs', + name: 'Enrich Search Jobs', + description: + 'Search LinkedIn job postings by keywords with filters for location, job type, workplace type, experience level, and company.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Enrich API key', + }, + keywords: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Search keywords (e.g., "software engineer")', + }, + location: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Location filter (e.g., London)', + }, + jobTypes: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Comma-separated job types (e.g., "full time, part time")', + }, + workplaceTypes: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Comma-separated workplace types (e.g., "on site, remote")', + }, + experienceLevels: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Comma-separated experience levels (e.g., "internship, associate")', + }, + companyIds: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Comma-separated LinkedIn company IDs to filter by (e.g., "2048, 3050")', + }, + timePosted: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Time filter (e.g., past_24hrs, past_week, past_month)', + }, + start: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of records to skip for pagination (default: 0)', + }, + }, + + request: { + url: (params) => { + const url = new URL('https://api.enrich.so/v1/api/search-jobs') + url.searchParams.append('keywords', params.keywords.trim()) + if (params.location) url.searchParams.append('location', params.location.trim()) + if (params.jobTypes) url.searchParams.append('jobTypes', params.jobTypes) + if (params.workplaceTypes) url.searchParams.append('workplaceTypes', params.workplaceTypes) + if (params.experienceLevels) { + url.searchParams.append('experienceLevels', params.experienceLevels) + } + if (params.companyIds) url.searchParams.append('companyIds', params.companyIds) + if (params.timePosted) url.searchParams.append('timePosted', params.timePosted) + if (params.start !== undefined) url.searchParams.append('start', String(params.start)) + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + const rawJobs = data.data ?? data.jobs ?? (Array.isArray(data) ? data : []) + + const jobs = rawJobs.map((job: any) => ({ + title: job.title ?? null, + companyName: job.company_name ?? job.companyName ?? null, + companyLink: job.company_link ?? job.companyLink ?? null, + companyLogo: job.company_logo_url ?? job.company_logo ?? job.companyLogo ?? null, + location: job.location ?? null, + url: job.url ?? job.job_url ?? job.jobUrl ?? null, + postedDate: job.posted_date ?? job.posting_date ?? job.postedDate ?? null, + postedTimestamp: + job.posted_timestamp ?? + job.posting_timestamp ?? + job.timestamp ?? + job.postedTimestamp ?? + null, + hiringStatus: job.hiring_status ?? job.hiringStatus ?? null, + criteria: job.criteria ?? job.employment_criteria ?? job.employmentCriteria ?? null, + })) + + return { + success: true, + output: { + count: data.count ?? jobs.length, + jobs, + }, + } + }, + + outputs: { + count: { + type: 'number', + description: 'Number of job postings returned', + }, + jobs: { + type: 'array', + description: 'Job postings', + items: { + type: 'object', + properties: { + title: { type: 'string', description: 'Job title' }, + companyName: { type: 'string', description: 'Hiring company name' }, + companyLink: { type: 'string', description: 'Company LinkedIn URL' }, + companyLogo: { type: 'string', description: 'Company logo URL' }, + location: { type: 'string', description: 'Job location' }, + url: { type: 'string', description: 'Job posting URL' }, + postedDate: { type: 'string', description: 'Date the job was posted' }, + postedTimestamp: { type: 'string', description: 'Timestamp the job was posted' }, + hiringStatus: { type: 'string', description: 'Hiring status' }, + criteria: { + type: 'object', + description: 'Employment criteria (seniority, type, function)', + }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/enrich/search_post_comments_by_url.ts b/apps/sim/tools/enrich/search_post_comments_by_url.ts new file mode 100644 index 00000000000..2e02cca2399 --- /dev/null +++ b/apps/sim/tools/enrich/search_post_comments_by_url.ts @@ -0,0 +1,143 @@ +import type { + EnrichSearchPostCommentsByUrlParams, + EnrichSearchPostCommentsResponse, +} from '@/tools/enrich/types' +import type { ToolConfig } from '@/tools/types' + +export const searchPostCommentsByUrlTool: ToolConfig< + EnrichSearchPostCommentsByUrlParams, + EnrichSearchPostCommentsResponse +> = { + id: 'enrich_search_post_comments_by_url', + name: 'Enrich Search Post Comments by URL', + description: 'Get comments on a LinkedIn post by its URL.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Enrich API key', + }, + postUrl: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'LinkedIn post URL (e.g., https://www.linkedin.com/posts/...)', + }, + page: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Page number (starts at 1, default: 1)', + }, + }, + + request: { + url: (params) => { + const url = new URL('https://api.enrich.so/v1/api/search-comment-by-url') + url.searchParams.append('post_url', params.postUrl.trim()) + if (params.page !== undefined) { + url.searchParams.append('page', String(params.page)) + } + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + const resultData = data.data ?? {} + + const comments = + resultData.data?.map((comment: any) => ({ + activityId: comment.activity_id ?? null, + commentary: comment.commentary ?? null, + linkedInUrl: comment.li_url ?? null, + commenter: { + profileId: comment.commenter?.profile_id ?? null, + firstName: comment.commenter?.first_name ?? null, + lastName: comment.commenter?.last_name ?? null, + subTitle: comment.commenter?.sub_title ?? comment.commenter?.subTitle ?? null, + profilePicture: + comment.commenter?.profile_picture ?? comment.commenter?.profilePicture ?? null, + backgroundImage: + comment.commenter?.background_image ?? comment.commenter?.backgroundImage ?? null, + entityUrn: comment.commenter?.entity_urn ?? comment.commenter?.entityUrn ?? null, + objectUrn: comment.commenter?.object_urn ?? comment.commenter?.objectUrn ?? null, + profileType: comment.commenter?.profile_type ?? comment.commenter?.profileType ?? null, + }, + reactionBreakdown: { + likes: comment.reaction_breakdown?.likes ?? 0, + empathy: comment.reaction_breakdown?.empathy ?? 0, + other: comment.reaction_breakdown?.other ?? 0, + }, + })) ?? [] + + return { + success: true, + output: { + page: resultData.page ?? 1, + totalPage: resultData.total_page ?? 1, + count: resultData.num ?? comments.length, + comments, + }, + } + }, + + outputs: { + page: { + type: 'number', + description: 'Current page number', + }, + totalPage: { + type: 'number', + description: 'Total number of pages', + }, + count: { + type: 'number', + description: 'Number of comments returned', + }, + comments: { + type: 'array', + description: 'Comments', + items: { + type: 'object', + properties: { + activityId: { type: 'string', description: 'Comment activity ID' }, + commentary: { type: 'string', description: 'Comment text' }, + linkedInUrl: { type: 'string', description: 'Link to comment' }, + commenter: { + type: 'object', + description: 'Commenter info', + properties: { + profileId: { type: 'string', description: 'Profile ID' }, + firstName: { type: 'string', description: 'First name' }, + lastName: { type: 'string', description: 'Last name' }, + subTitle: { type: 'string', description: 'Subtitle/headline' }, + profilePicture: { type: 'string', description: 'Profile picture URL' }, + backgroundImage: { type: 'string', description: 'Background image URL' }, + entityUrn: { type: 'string', description: 'Entity URN' }, + objectUrn: { type: 'string', description: 'Object URN' }, + profileType: { type: 'string', description: 'Profile type' }, + }, + }, + reactionBreakdown: { + type: 'object', + description: 'Reactions on the comment', + properties: { + likes: { type: 'number', description: 'Number of likes' }, + empathy: { type: 'number', description: 'Number of empathy reactions' }, + other: { type: 'number', description: 'Number of other reactions' }, + }, + }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/enrich/search_post_reactions_by_url.ts b/apps/sim/tools/enrich/search_post_reactions_by_url.ts new file mode 100644 index 00000000000..8c84c10677f --- /dev/null +++ b/apps/sim/tools/enrich/search_post_reactions_by_url.ts @@ -0,0 +1,121 @@ +import type { + EnrichSearchPostReactionsByUrlParams, + EnrichSearchPostReactionsResponse, +} from '@/tools/enrich/types' +import type { ToolConfig } from '@/tools/types' + +export const searchPostReactionsByUrlTool: ToolConfig< + EnrichSearchPostReactionsByUrlParams, + EnrichSearchPostReactionsResponse +> = { + id: 'enrich_search_post_reactions_by_url', + name: 'Enrich Search Post Reactions by URL', + description: 'Get reactions on a LinkedIn post by its URL, filtered by reaction type.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Enrich API key', + }, + postUrl: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'LinkedIn post URL (e.g., https://www.linkedin.com/posts/...)', + }, + reactionType: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'Reaction type filter: all, like, love, celebrate, insightful, or funny (default: all)', + }, + page: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'Page number (starts at 1)', + }, + }, + + request: { + url: (params) => { + const url = new URL('https://api.enrich.so/v1/api/search-reaction-by-url') + url.searchParams.append('post_url', params.postUrl.trim()) + url.searchParams.append('reaction_type', params.reactionType) + url.searchParams.append('page', String(params.page)) + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + const resultData = data.data ?? {} + + const reactions = + resultData.data?.map((reaction: any) => ({ + reactionType: reaction.reaction_type ?? '', + reactor: { + name: reaction.reactor?.name ?? null, + subTitle: reaction.reactor?.sub_title ?? null, + profileId: reaction.reactor?.profile_id ?? null, + profilePicture: reaction.reactor?.profile_picture ?? null, + linkedInUrl: reaction.reactor?.li_url ?? null, + }, + })) ?? [] + + return { + success: true, + output: { + page: resultData.page ?? 1, + totalPage: resultData.total_page ?? 1, + count: resultData.num ?? reactions.length, + reactions, + }, + } + }, + + outputs: { + page: { + type: 'number', + description: 'Current page number', + }, + totalPage: { + type: 'number', + description: 'Total number of pages', + }, + count: { + type: 'number', + description: 'Number of reactions returned', + }, + reactions: { + type: 'array', + description: 'Reactions', + items: { + type: 'object', + properties: { + reactionType: { type: 'string', description: 'Type of reaction' }, + reactor: { + type: 'object', + description: 'Person who reacted', + properties: { + name: { type: 'string', description: 'Name' }, + subTitle: { type: 'string', description: 'Job title' }, + profileId: { type: 'string', description: 'Profile ID' }, + profilePicture: { type: 'string', description: 'Profile picture URL' }, + linkedInUrl: { type: 'string', description: 'LinkedIn URL' }, + }, + }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/enrich/types.ts b/apps/sim/tools/enrich/types.ts index f635ae0f86b..d1980181daa 100644 --- a/apps/sim/tools/enrich/types.ts +++ b/apps/sim/tools/enrich/types.ts @@ -740,3 +740,43 @@ export interface EnrichSearchLogoResponse extends ToolResponse { domain: string } } + +export interface EnrichSearchJobsParams extends EnrichBaseParams { + keywords: string + location?: string + jobTypes?: string + workplaceTypes?: string + experienceLevels?: string + companyIds?: string + timePosted?: string + start?: number +} + +export interface EnrichSearchJobsResponse extends ToolResponse { + output: { + count: number + jobs: Array<{ + title: string | null + companyName: string | null + companyLink: string | null + companyLogo: string | null + location: string | null + url: string | null + postedDate: string | null + postedTimestamp: string | null + hiringStatus: string | null + criteria: Record | null + }> + } +} + +export interface EnrichSearchPostReactionsByUrlParams extends EnrichBaseParams { + postUrl: string + reactionType: 'all' | 'like' | 'love' | 'celebrate' | 'insightful' | 'funny' + page: number +} + +export interface EnrichSearchPostCommentsByUrlParams extends EnrichBaseParams { + postUrl: string + page?: number +} diff --git a/apps/sim/tools/fireflies/create_bite.ts b/apps/sim/tools/fireflies/create_bite.ts index 26bb8762822..ecab7cb8897 100644 --- a/apps/sim/tools/fireflies/create_bite.ts +++ b/apps/sim/tools/fireflies/create_bite.ts @@ -102,7 +102,7 @@ export const firefliesCreateBiteTool: ToolConfig< $summary: String ) { createBite( - transcript_Id: $transcriptId + transcript_id: $transcriptId start_time: $startTime end_time: $endTime name: $name diff --git a/apps/sim/tools/fireflies/delete_transcript.ts b/apps/sim/tools/fireflies/delete_transcript.ts index bf5aa4d5cfd..df0d9985c73 100644 --- a/apps/sim/tools/fireflies/delete_transcript.ts +++ b/apps/sim/tools/fireflies/delete_transcript.ts @@ -49,7 +49,12 @@ export const firefliesDeleteTranscriptTool: ToolConfig< query: ` mutation DeleteTranscript($id: String!) { deleteTranscript(id: $id) { - success + id + title + date + duration + host_email + organizer_email } } `, @@ -71,11 +76,27 @@ export const firefliesDeleteTranscriptTool: ToolConfig< } } - const result = data.data?.deleteTranscript + const deleted = data.data?.deleteTranscript + if (!deleted) { + return { + success: false, + error: 'Failed to delete transcript', + output: { success: false }, + } + } + return { - success: result?.success ?? false, + success: true, output: { - success: result?.success ?? false, + success: true, + transcript: { + id: deleted.id, + title: deleted.title ?? null, + date: deleted.date ?? null, + duration: deleted.duration ?? null, + host_email: deleted.host_email ?? null, + organizer_email: deleted.organizer_email ?? null, + }, }, } }, @@ -85,5 +106,18 @@ export const firefliesDeleteTranscriptTool: ToolConfig< type: 'boolean', description: 'Whether the transcript was successfully deleted', }, + transcript: { + type: 'object', + description: 'The deleted transcript', + optional: true, + properties: { + id: { type: 'string', description: 'Transcript ID' }, + title: { type: 'string', description: 'Meeting title' }, + date: { type: 'number', description: 'Meeting timestamp' }, + duration: { type: 'number', description: 'Meeting duration' }, + host_email: { type: 'string', description: 'Host email address' }, + organizer_email: { type: 'string', description: 'Organizer email address' }, + }, + }, }, } diff --git a/apps/sim/tools/fireflies/types.ts b/apps/sim/tools/fireflies/types.ts index e2661912ad9..147aa807260 100644 --- a/apps/sim/tools/fireflies/types.ts +++ b/apps/sim/tools/fireflies/types.ts @@ -229,6 +229,14 @@ export interface FirefliesUploadAudioResponse extends ToolResponse { export interface FirefliesDeleteTranscriptResponse extends ToolResponse { output: { success?: boolean + transcript?: { + id: string + title: string | null + date: number | null + duration: number | null + host_email: string | null + organizer_email: string | null + } } } diff --git a/apps/sim/tools/gong/aggregate_by_period.ts b/apps/sim/tools/gong/aggregate_by_period.ts new file mode 100644 index 00000000000..cc8b0576c7f --- /dev/null +++ b/apps/sim/tools/gong/aggregate_by_period.ts @@ -0,0 +1,219 @@ +import type { GongAggregateByPeriodParams, GongAggregateByPeriodResponse } from '@/tools/gong/types' +import { getGongErrorMessage, parseGongIdList } from '@/tools/gong/utils' +import type { ToolConfig } from '@/tools/types' + +export const aggregateByPeriodTool: ToolConfig< + GongAggregateByPeriodParams, + GongAggregateByPeriodResponse +> = { + id: 'gong_aggregate_by_period', + name: 'Gong Aggregate by Period', + description: + 'Retrieve aggregated user activity grouped into time periods (day, week, month, quarter, year) by date range from Gong.', + version: '1.0.0', + + params: { + accessKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Gong API Access Key', + }, + accessKeySecret: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Gong API Access Key Secret', + }, + aggregationPeriod: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'Calendar period to group activity by: DAY, WEEK, MONTH, QUARTER, or YEAR (week starts Monday)', + }, + userIds: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Comma-separated list of Gong user IDs (up to 20 digits each)', + }, + fromDate: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Start date in YYYY-MM-DD format (inclusive, in company timezone)', + }, + toDate: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'End date in YYYY-MM-DD format (exclusive, in company timezone, cannot exceed current day)', + }, + cursor: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Pagination cursor from a previous response', + }, + }, + + request: { + url: 'https://api.gong.io/v2/stats/activity/aggregate-by-period', + method: 'POST', + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Basic ${btoa(`${params.accessKey}:${params.accessKeySecret}`)}`, + }), + body: (params) => { + const filter: Record = { + fromDate: params.fromDate.trim(), + toDate: params.toDate.trim(), + } + const userIds = parseGongIdList(params.userIds) + if (userIds) filter.userIds = userIds + const body: Record = { + aggregationPeriod: params.aggregationPeriod.trim(), + filter, + } + if (params.cursor?.trim()) body.cursor = params.cursor.trim() + return body + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + throw new Error(getGongErrorMessage(data, 'Failed to get aggregate-by-period activity')) + } + const usersAggregateActivity = (data.usersAggregateActivity ?? []).map( + (ua: Record) => ({ + userId: ua.userId ?? '', + userEmailAddress: ua.userEmailAddress ?? null, + userAggregateActivity: ( + (ua.userAggregateActivity as Record[] | undefined) ?? [] + ).map((period: Record) => ({ + fromDate: period.fromDate ?? null, + toDate: period.toDate ?? null, + callsAsHost: period.callsAsHost ?? null, + callsAttended: period.callsAttended ?? null, + callsGaveFeedback: period.callsGaveFeedback ?? null, + callsReceivedFeedback: period.callsReceivedFeedback ?? null, + callsRequestedFeedback: period.callsRequestedFeedback ?? null, + callsScorecardsFilled: period.callsScorecardsFilled ?? null, + callsScorecardsReceived: period.callsScorecardsReceived ?? null, + ownCallsListenedTo: period.ownCallsListenedTo ?? null, + othersCallsListenedTo: period.othersCallsListenedTo ?? null, + callsSharedInternally: period.callsSharedInternally ?? null, + callsSharedExternally: period.callsSharedExternally ?? null, + callsCommentsGiven: period.callsCommentsGiven ?? null, + callsCommentsReceived: period.callsCommentsReceived ?? null, + callsMarkedAsFeedbackGiven: period.callsMarkedAsFeedbackGiven ?? null, + callsMarkedAsFeedbackReceived: period.callsMarkedAsFeedbackReceived ?? null, + })), + }) + ) + return { + success: true, + output: { + requestId: data.requestId ?? null, + usersAggregateActivity, + cursor: data.records?.cursor ?? null, + }, + } + }, + + outputs: { + requestId: { + type: 'string', + description: 'A Gong request reference ID for troubleshooting purposes', + optional: true, + }, + usersAggregateActivity: { + type: 'array', + description: + 'Aggregated activity per user, one item per consecutive time period in the range', + items: { + type: 'object', + properties: { + userId: { type: 'string', description: "Gong's unique numeric identifier for the user" }, + userEmailAddress: { type: 'string', description: 'Email address of the Gong user' }, + userAggregateActivity: { + type: 'array', + description: 'Activity counts per time period', + items: { + type: 'object', + properties: { + fromDate: { type: 'string', description: 'Start of the period (ISO-8601)' }, + toDate: { type: 'string', description: 'End of the period (ISO-8601)' }, + callsAsHost: { type: 'number', description: 'Calls the user hosted' }, + callsAttended: { + type: 'number', + description: 'Calls the user attended (not host)', + }, + callsGaveFeedback: { + type: 'number', + description: 'Calls the user gave feedback on', + }, + callsReceivedFeedback: { + type: 'number', + description: 'Calls the user received feedback on', + }, + callsRequestedFeedback: { + type: 'number', + description: 'Calls the user requested feedback on', + }, + callsScorecardsFilled: { + type: 'number', + description: 'Scorecards the user completed', + }, + callsScorecardsReceived: { + type: 'number', + description: "Calls where someone filled a scorecard on the user's calls", + }, + ownCallsListenedTo: { + type: 'number', + description: "The user's own calls the user listened to", + }, + othersCallsListenedTo: { + type: 'number', + description: "Other users' calls the user listened to", + }, + callsSharedInternally: { + type: 'number', + description: 'Calls the user shared internally', + }, + callsSharedExternally: { + type: 'number', + description: 'Calls the user shared externally', + }, + callsCommentsGiven: { + type: 'number', + description: 'Calls the user commented on', + }, + callsCommentsReceived: { + type: 'number', + description: "Calls where the user's calls received a comment", + }, + callsMarkedAsFeedbackGiven: { + type: 'number', + description: 'Calls the user marked as reviewed', + }, + callsMarkedAsFeedbackReceived: { + type: 'number', + description: "The user's calls marked as reviewed by others", + }, + }, + }, + }, + }, + }, + }, + cursor: { + type: 'string', + description: 'Pagination cursor for the next page', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/gong/day_by_day_activity.ts b/apps/sim/tools/gong/day_by_day_activity.ts new file mode 100644 index 00000000000..8bc443f99cc --- /dev/null +++ b/apps/sim/tools/gong/day_by_day_activity.ts @@ -0,0 +1,208 @@ +import type { GongDayByDayActivityParams, GongDayByDayActivityResponse } from '@/tools/gong/types' +import { getGongErrorMessage, parseGongIdList } from '@/tools/gong/utils' +import type { ToolConfig } from '@/tools/types' + +export const dayByDayActivityTool: ToolConfig< + GongDayByDayActivityParams, + GongDayByDayActivityResponse +> = { + id: 'gong_day_by_day_activity', + name: 'Gong Day-by-Day Activity', + description: + 'Retrieve detailed day-by-day activity (call IDs per activity type) for users by date range from Gong.', + version: '1.0.0', + + params: { + accessKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Gong API Access Key', + }, + accessKeySecret: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Gong API Access Key Secret', + }, + userIds: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Comma-separated list of Gong user IDs (up to 20 digits each)', + }, + fromDate: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Start date in YYYY-MM-DD format (inclusive, in company timezone)', + }, + toDate: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'End date in YYYY-MM-DD format (exclusive, in company timezone, cannot exceed current day)', + }, + cursor: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Pagination cursor from a previous response', + }, + }, + + request: { + url: 'https://api.gong.io/v2/stats/activity/day-by-day', + method: 'POST', + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Basic ${btoa(`${params.accessKey}:${params.accessKeySecret}`)}`, + }), + body: (params) => { + const filter: Record = { + fromDate: params.fromDate.trim(), + toDate: params.toDate.trim(), + } + const userIds = parseGongIdList(params.userIds) + if (userIds) filter.userIds = userIds + const body: Record = { filter } + if (params.cursor?.trim()) body.cursor = params.cursor.trim() + return body + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + throw new Error(getGongErrorMessage(data, 'Failed to get day-by-day activity')) + } + const usersDetailedActivities = (data.usersDetailedActivities ?? []).map( + (ud: Record) => ({ + userId: ud.userId ?? '', + userEmailAddress: ud.userEmailAddress ?? null, + userDailyActivityStats: ( + (ud.userDailyActivityStats as Record[] | undefined) ?? [] + ).map((day: Record) => ({ + fromDate: day.fromDate ?? null, + toDate: day.toDate ?? null, + callsAsHost: day.callsAsHost ?? [], + callsAttended: day.callsAttended ?? [], + callsGaveFeedback: day.callsGaveFeedback ?? [], + callsReceivedFeedback: day.callsReceivedFeedback ?? [], + callsRequestedFeedback: day.callsRequestedFeedback ?? [], + callsScorecardsFilled: day.callsScorecardsFilled ?? [], + callsScorecardsReceived: day.callsScorecardsReceived ?? [], + ownCallsListenedTo: day.ownCallsListenedTo ?? [], + othersCallsListenedTo: day.othersCallsListenedTo ?? [], + callsSharedInternally: day.callsSharedInternally ?? [], + callsSharedExternally: day.callsSharedExternally ?? [], + callsCommentsGiven: day.callsCommentsGiven ?? [], + callsCommentsReceived: day.callsCommentsReceived ?? [], + callsMarkedAsFeedbackGiven: day.callsMarkedAsFeedbackGiven ?? [], + callsMarkedAsFeedbackReceived: day.callsMarkedAsFeedbackReceived ?? [], + })), + }) + ) + return { + success: true, + output: { + requestId: data.requestId ?? null, + usersDetailedActivities, + cursor: data.records?.cursor ?? null, + }, + } + }, + + outputs: { + requestId: { + type: 'string', + description: 'A Gong request reference ID for troubleshooting purposes', + optional: true, + }, + usersDetailedActivities: { + type: 'array', + description: 'Day-by-day activity per user, with call IDs grouped by activity type', + items: { + type: 'object', + properties: { + userId: { type: 'string', description: "Gong's unique numeric identifier for the user" }, + userEmailAddress: { type: 'string', description: 'Email address of the Gong user' }, + userDailyActivityStats: { + type: 'array', + description: 'One record per day in the date range', + items: { + type: 'object', + properties: { + fromDate: { type: 'string', description: 'Start of the day (ISO-8601)' }, + toDate: { type: 'string', description: 'End of the day (ISO-8601)' }, + callsAsHost: { type: 'array', description: 'IDs of calls the user hosted' }, + callsAttended: { + type: 'array', + description: 'IDs of calls the user attended (not host)', + }, + callsGaveFeedback: { + type: 'array', + description: 'IDs of calls the user gave feedback on', + }, + callsReceivedFeedback: { + type: 'array', + description: 'IDs of calls the user received feedback on', + }, + callsRequestedFeedback: { + type: 'array', + description: 'IDs of calls the user requested feedback on', + }, + callsScorecardsFilled: { + type: 'array', + description: 'IDs of calls the user filled scorecards on', + }, + callsScorecardsReceived: { + type: 'array', + description: "IDs of the user's calls that received a scorecard", + }, + ownCallsListenedTo: { + type: 'array', + description: "IDs of the user's own calls the user listened to", + }, + othersCallsListenedTo: { + type: 'array', + description: "IDs of other users' calls the user listened to", + }, + callsSharedInternally: { + type: 'array', + description: 'IDs of calls the user shared internally', + }, + callsSharedExternally: { + type: 'array', + description: 'IDs of calls the user shared externally', + }, + callsCommentsGiven: { + type: 'array', + description: 'IDs of calls the user commented on', + }, + callsCommentsReceived: { + type: 'array', + description: "IDs of the user's calls that received a comment", + }, + callsMarkedAsFeedbackGiven: { + type: 'array', + description: 'IDs of calls the user marked as reviewed', + }, + callsMarkedAsFeedbackReceived: { + type: 'array', + description: "IDs of the user's calls marked as reviewed by others", + }, + }, + }, + }, + }, + }, + }, + cursor: { + type: 'string', + description: 'Pagination cursor for the next page', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/gong/get_extensive_calls.ts b/apps/sim/tools/gong/get_extensive_calls.ts index aee5065e219..eda3bcd3ff0 100644 --- a/apps/sim/tools/gong/get_extensive_calls.ts +++ b/apps/sim/tools/gong/get_extensive_calls.ts @@ -8,7 +8,8 @@ export const getExtensiveCallsTool: ToolConfig< > = { id: 'gong_get_extensive_calls', name: 'Gong Get Extensive Calls', - description: 'Retrieve detailed call data including trackers, topics, and highlights from Gong.', + description: + 'Retrieve detailed call data including trackers, topics, highlights, and AI spotlight content (brief, outline, key points, call outcome) from Gong.', version: '1.0.0', params: { @@ -89,6 +90,10 @@ export const getExtensiveCallsTool: ToolConfig< trackers: true, trackerOccurrences: true, highlights: true, + brief: true, + outline: true, + keyPoints: true, + callOutcome: true, }, collaboration: { publicComments: true }, interaction: { @@ -194,6 +199,45 @@ export const getExtensiveCallsTool: ToolConfig< type: 'object', description: 'Call content data', properties: { + brief: { + type: 'string', + description: 'AI-generated brief summary of the call (Call Spotlight)', + }, + outline: { + type: 'array', + description: 'AI-generated call outline sections', + items: { + type: 'object', + properties: { + section: { type: 'string', description: 'Outline section name' }, + startTime: { + type: 'number', + description: 'Section start in seconds from call start', + }, + duration: { type: 'number', description: 'Section duration in seconds' }, + items: { type: 'array', description: 'Bullet items within the section' }, + }, + }, + }, + keyPoints: { + type: 'array', + description: 'AI-generated key points of the call', + items: { + type: 'object', + properties: { + text: { type: 'string', description: 'Key point text' }, + }, + }, + }, + callOutcome: { + type: 'object', + description: 'AI-determined call outcome (Call Spotlight)', + properties: { + id: { type: 'string', description: 'Outcome category ID' }, + category: { type: 'string', description: 'Outcome category name' }, + name: { type: 'string', description: 'Outcome name' }, + }, + }, structure: { type: 'array', description: 'Call agenda parts', diff --git a/apps/sim/tools/gong/index.ts b/apps/sim/tools/gong/index.ts index c838bd8e075..36b5e67de08 100644 --- a/apps/sim/tools/gong/index.ts +++ b/apps/sim/tools/gong/index.ts @@ -1,6 +1,8 @@ import { aggregateActivityTool } from '@/tools/gong/aggregate_activity' +import { aggregateByPeriodTool } from '@/tools/gong/aggregate_by_period' import { answeredScorecardsTool } from '@/tools/gong/answered_scorecards' import { createCallTool } from '@/tools/gong/create_call' +import { dayByDayActivityTool } from '@/tools/gong/day_by_day_activity' import { getCallTool } from '@/tools/gong/get_call' import { getCallTranscriptTool } from '@/tools/gong/get_call_transcript' import { getCoachingTool } from '@/tools/gong/get_coaching' @@ -26,6 +28,8 @@ export const gongGetExtensiveCallsTool = getExtensiveCallsTool export const gongListUsersTool = listUsersTool export const gongGetUserTool = getUserTool export const gongAggregateActivityTool = aggregateActivityTool +export const gongDayByDayActivityTool = dayByDayActivityTool +export const gongAggregateByPeriodTool = aggregateByPeriodTool export const gongInteractionStatsTool = interactionStatsTool export const gongAnsweredScorecardsTool = answeredScorecardsTool export const gongListLibraryFoldersTool = listLibraryFoldersTool diff --git a/apps/sim/tools/gong/types.ts b/apps/sim/tools/gong/types.ts index 55d7c9b3c94..97151084576 100644 --- a/apps/sim/tools/gong/types.ts +++ b/apps/sim/tools/gong/types.ts @@ -297,6 +297,91 @@ export interface GongInteractionStatsResponse extends ToolResponse { } } +/** Day-by-Day Activity */ +export interface GongDayByDayActivityParams extends GongBaseParams { + userIds?: string + fromDate: string + toDate: string + cursor?: string +} + +interface GongDailyActivity { + fromDate: string | null + toDate: string | null + callsAsHost: string[] + callsAttended: string[] + callsGaveFeedback: string[] + callsReceivedFeedback: string[] + callsRequestedFeedback: string[] + callsScorecardsFilled: string[] + callsScorecardsReceived: string[] + ownCallsListenedTo: string[] + othersCallsListenedTo: string[] + callsSharedInternally: string[] + callsSharedExternally: string[] + callsCommentsGiven: string[] + callsCommentsReceived: string[] + callsMarkedAsFeedbackGiven: string[] + callsMarkedAsFeedbackReceived: string[] +} + +interface GongUserDayByDayActivity { + userId: string + userEmailAddress: string | null + userDailyActivityStats: GongDailyActivity[] +} + +export interface GongDayByDayActivityResponse extends ToolResponse { + output: { + requestId: string | null + usersDetailedActivities: GongUserDayByDayActivity[] + cursor: string | null + } +} + +/** Aggregate by Period */ +export interface GongAggregateByPeriodParams extends GongBaseParams { + aggregationPeriod: string + userIds?: string + fromDate: string + toDate: string + cursor?: string +} + +interface GongPeriodActivity { + fromDate: string | null + toDate: string | null + callsAsHost: number | null + callsAttended: number | null + callsGaveFeedback: number | null + callsReceivedFeedback: number | null + callsRequestedFeedback: number | null + callsScorecardsFilled: number | null + callsScorecardsReceived: number | null + ownCallsListenedTo: number | null + othersCallsListenedTo: number | null + callsSharedInternally: number | null + callsSharedExternally: number | null + callsCommentsGiven: number | null + callsCommentsReceived: number | null + callsMarkedAsFeedbackGiven: number | null + callsMarkedAsFeedbackReceived: number | null +} + +interface GongUserAggregateByPeriod { + userId: string + userEmailAddress: string | null + userAggregateActivity: GongPeriodActivity[] +} + +export interface GongAggregateByPeriodResponse extends ToolResponse { + output: { + requestId: string | null + usersAggregateActivity: GongUserAggregateByPeriod[] + cursor: string | null + } +} + /** Answered Scorecards */ export interface GongAnsweredScorecardsParams extends GongBaseParams { callFromDate?: string @@ -620,6 +705,8 @@ export type GongResponse = | GongListUsersResponse | GongGetUserResponse | GongAggregateActivityResponse + | GongDayByDayActivityResponse + | GongAggregateByPeriodResponse | GongInteractionStatsResponse | GongAnsweredScorecardsResponse | GongListLibraryFoldersResponse diff --git a/apps/sim/tools/okta/activate_user.ts b/apps/sim/tools/okta/activate_user.ts index 9f8d40cb0d5..e11d441bdc0 100644 --- a/apps/sim/tools/okta/activate_user.ts +++ b/apps/sim/tools/okta/activate_user.ts @@ -47,7 +47,7 @@ export const oktaActivateUserTool: ToolConfig { const domain = validateOktaDomain(params.domain) const sendEmail = params.sendEmail ?? true - return `https://${domain}/api/v1/users/${encodeURIComponent(params.userId)}/lifecycle/activate?sendEmail=${sendEmail}` + return `https://${domain}/api/v1/users/${encodeURIComponent(params.userId.trim())}/lifecycle/activate?sendEmail=${sendEmail}` }, method: 'POST', headers: (params) => ({ diff --git a/apps/sim/tools/okta/add_user_to_group.ts b/apps/sim/tools/okta/add_user_to_group.ts index 2122344b884..dbadf1255cf 100644 --- a/apps/sim/tools/okta/add_user_to_group.ts +++ b/apps/sim/tools/okta/add_user_to_group.ts @@ -48,7 +48,7 @@ export const oktaAddUserToGroupTool: ToolConfig< request: { url: (params) => { const domain = validateOktaDomain(params.domain) - return `https://${domain}/api/v1/groups/${encodeURIComponent(params.groupId)}/users/${encodeURIComponent(params.userId)}` + return `https://${domain}/api/v1/groups/${encodeURIComponent(params.groupId.trim())}/users/${encodeURIComponent(params.userId.trim())}` }, method: 'PUT', headers: (params) => ({ diff --git a/apps/sim/tools/okta/deactivate_user.ts b/apps/sim/tools/okta/deactivate_user.ts index d6aaf782cd6..ad1be71e446 100644 --- a/apps/sim/tools/okta/deactivate_user.ts +++ b/apps/sim/tools/okta/deactivate_user.ts @@ -50,7 +50,7 @@ export const oktaDeactivateUserTool: ToolConfig< url: (params) => { const domain = validateOktaDomain(params.domain) const sendEmail = params.sendEmail === true - return `https://${domain}/api/v1/users/${encodeURIComponent(params.userId)}/lifecycle/deactivate?sendEmail=${sendEmail}` + return `https://${domain}/api/v1/users/${encodeURIComponent(params.userId.trim())}/lifecycle/deactivate?sendEmail=${sendEmail}` }, method: 'POST', headers: (params) => ({ diff --git a/apps/sim/tools/okta/delete_group.ts b/apps/sim/tools/okta/delete_group.ts index f1b0363fa5e..762ac479147 100644 --- a/apps/sim/tools/okta/delete_group.ts +++ b/apps/sim/tools/okta/delete_group.ts @@ -40,7 +40,7 @@ export const oktaDeleteGroupTool: ToolConfig { const domain = validateOktaDomain(params.domain) - return `https://${domain}/api/v1/groups/${encodeURIComponent(params.groupId)}` + return `https://${domain}/api/v1/groups/${encodeURIComponent(params.groupId.trim())}` }, method: 'DELETE', headers: (params) => ({ diff --git a/apps/sim/tools/okta/delete_user.ts b/apps/sim/tools/okta/delete_user.ts index 5c58fb9c429..f8e59145d99 100644 --- a/apps/sim/tools/okta/delete_user.ts +++ b/apps/sim/tools/okta/delete_user.ts @@ -43,7 +43,7 @@ export const oktaDeleteUserTool: ToolConfig { const domain = validateOktaDomain(params.domain) const sendEmail = params.sendEmail === true - return `https://${domain}/api/v1/users/${encodeURIComponent(params.userId)}?sendEmail=${sendEmail}` + return `https://${domain}/api/v1/users/${encodeURIComponent(params.userId.trim())}?sendEmail=${sendEmail}` }, method: 'DELETE', headers: (params) => ({ diff --git a/apps/sim/tools/okta/get_group.ts b/apps/sim/tools/okta/get_group.ts index 04c3ac67838..1e617ee1253 100644 --- a/apps/sim/tools/okta/get_group.ts +++ b/apps/sim/tools/okta/get_group.ts @@ -40,7 +40,7 @@ export const oktaGetGroupTool: ToolConfig { const domain = validateOktaDomain(params.domain) - return `https://${domain}/api/v1/groups/${encodeURIComponent(params.groupId)}` + return `https://${domain}/api/v1/groups/${encodeURIComponent(params.groupId.trim())}` }, method: 'GET', headers: (params) => ({ diff --git a/apps/sim/tools/okta/get_user.ts b/apps/sim/tools/okta/get_user.ts index 516fd312e78..7684689b229 100644 --- a/apps/sim/tools/okta/get_user.ts +++ b/apps/sim/tools/okta/get_user.ts @@ -40,7 +40,7 @@ export const oktaGetUserTool: ToolConfig request: { url: (params) => { const domain = validateOktaDomain(params.domain) - return `https://${domain}/api/v1/users/${encodeURIComponent(params.userId)}` + return `https://${domain}/api/v1/users/${encodeURIComponent(params.userId.trim())}` }, method: 'GET', headers: (params) => ({ diff --git a/apps/sim/tools/okta/list_group_members.ts b/apps/sim/tools/okta/list_group_members.ts index e0b5a036959..7e7368305e8 100644 --- a/apps/sim/tools/okta/list_group_members.ts +++ b/apps/sim/tools/okta/list_group_members.ts @@ -54,7 +54,7 @@ export const oktaListGroupMembersTool: ToolConfig< if (params.limit) queryParams.append('limit', params.limit.toString()) const queryString = queryParams.toString() - const base = `https://${domain}/api/v1/groups/${encodeURIComponent(params.groupId)}/users` + const base = `https://${domain}/api/v1/groups/${encodeURIComponent(params.groupId.trim())}/users` return queryString ? `${base}?${queryString}` : base }, method: 'GET', diff --git a/apps/sim/tools/okta/remove_user_from_group.ts b/apps/sim/tools/okta/remove_user_from_group.ts index 7a3c7c42dc3..4a64d83536c 100644 --- a/apps/sim/tools/okta/remove_user_from_group.ts +++ b/apps/sim/tools/okta/remove_user_from_group.ts @@ -48,7 +48,7 @@ export const oktaRemoveUserFromGroupTool: ToolConfig< request: { url: (params) => { const domain = validateOktaDomain(params.domain) - return `https://${domain}/api/v1/groups/${encodeURIComponent(params.groupId)}/users/${encodeURIComponent(params.userId)}` + return `https://${domain}/api/v1/groups/${encodeURIComponent(params.groupId.trim())}/users/${encodeURIComponent(params.userId.trim())}` }, method: 'DELETE', headers: (params) => ({ diff --git a/apps/sim/tools/okta/reset_password.ts b/apps/sim/tools/okta/reset_password.ts index f485ea047b2..8a1d5f6633b 100644 --- a/apps/sim/tools/okta/reset_password.ts +++ b/apps/sim/tools/okta/reset_password.ts @@ -48,7 +48,7 @@ export const oktaResetPasswordTool: ToolConfig { const domain = validateOktaDomain(params.domain) const sendEmail = params.sendEmail ?? true - return `https://${domain}/api/v1/users/${encodeURIComponent(params.userId)}/lifecycle/reset_password?sendEmail=${sendEmail}` + return `https://${domain}/api/v1/users/${encodeURIComponent(params.userId.trim())}/lifecycle/reset_password?sendEmail=${sendEmail}` }, method: 'POST', headers: (params) => ({ diff --git a/apps/sim/tools/okta/suspend_user.ts b/apps/sim/tools/okta/suspend_user.ts index 1615905b7c4..aad691f9e0a 100644 --- a/apps/sim/tools/okta/suspend_user.ts +++ b/apps/sim/tools/okta/suspend_user.ts @@ -40,7 +40,7 @@ export const oktaSuspendUserTool: ToolConfig { const domain = validateOktaDomain(params.domain) - return `https://${domain}/api/v1/users/${encodeURIComponent(params.userId)}/lifecycle/suspend` + return `https://${domain}/api/v1/users/${encodeURIComponent(params.userId.trim())}/lifecycle/suspend` }, method: 'POST', headers: (params) => ({ diff --git a/apps/sim/tools/okta/unsuspend_user.ts b/apps/sim/tools/okta/unsuspend_user.ts index 3b3a2f7569e..cd079a02bba 100644 --- a/apps/sim/tools/okta/unsuspend_user.ts +++ b/apps/sim/tools/okta/unsuspend_user.ts @@ -41,7 +41,7 @@ export const oktaUnsuspendUserTool: ToolConfig { const domain = validateOktaDomain(params.domain) - return `https://${domain}/api/v1/users/${encodeURIComponent(params.userId)}/lifecycle/unsuspend` + return `https://${domain}/api/v1/users/${encodeURIComponent(params.userId.trim())}/lifecycle/unsuspend` }, method: 'POST', headers: (params) => ({ diff --git a/apps/sim/tools/okta/update_group.ts b/apps/sim/tools/okta/update_group.ts index dd8d2d6c8bc..34daf77bbde 100644 --- a/apps/sim/tools/okta/update_group.ts +++ b/apps/sim/tools/okta/update_group.ts @@ -53,7 +53,7 @@ export const oktaUpdateGroupTool: ToolConfig { const domain = validateOktaDomain(params.domain) - return `https://${domain}/api/v1/groups/${encodeURIComponent(params.groupId)}` + return `https://${domain}/api/v1/groups/${encodeURIComponent(params.groupId.trim())}` }, method: 'PUT', headers: (params) => ({ diff --git a/apps/sim/tools/okta/update_user.ts b/apps/sim/tools/okta/update_user.ts index 7775872b7cc..ea095d0df54 100644 --- a/apps/sim/tools/okta/update_user.ts +++ b/apps/sim/tools/okta/update_user.ts @@ -82,7 +82,7 @@ export const oktaUpdateUserTool: ToolConfig { const domain = validateOktaDomain(params.domain) - return `https://${domain}/api/v1/users/${encodeURIComponent(params.userId)}` + return `https://${domain}/api/v1/users/${encodeURIComponent(params.userId.trim())}` }, method: 'POST', headers: (params) => ({ diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 73d75834776..740202ee7fd 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -537,11 +537,15 @@ import { import { databricksCancelRunTool, databricksExecuteSqlTool, + databricksGetClusterTool, + databricksGetJobTool, databricksGetRunOutputTool, databricksGetRunTool, + databricksGetStatementTool, databricksListClustersTool, databricksListJobsTool, databricksListRunsTool, + databricksListWarehousesTool, databricksRunJobTool, } from '@/tools/databricks' import { @@ -632,10 +636,16 @@ import { } from '@/tools/dropbox' import { chainOfThoughtTool, predictTool, reactTool } from '@/tools/dspy' import { + dubBulkCreateLinksTool, + dubBulkDeleteLinksTool, + dubBulkUpdateLinksTool, dubCreateLinkTool, dubDeleteLinkTool, dubGetAnalyticsTool, + dubGetEventsTool, + dubGetLinksCountTool, dubGetLinkTool, + dubGetQrCodeTool, dubListLinksTool, dubUpdateLinkTool, dubUpsertLinkTool, @@ -703,10 +713,13 @@ import { enrichSearchCompanyActivitiesTool, enrichSearchCompanyEmployeesTool, enrichSearchCompanyTool, + enrichSearchJobsTool, enrichSearchLogoTool, enrichSearchPeopleActivitiesTool, enrichSearchPeopleTool, + enrichSearchPostCommentsByUrlTool, enrichSearchPostCommentsTool, + enrichSearchPostReactionsByUrlTool, enrichSearchPostReactionsTool, enrichSearchPostsTool, enrichSearchSimilarCompaniesTool, @@ -1023,8 +1036,10 @@ import { } from '@/tools/gmail' import { gongAggregateActivityTool, + gongAggregateByPeriodTool, gongAnsweredScorecardsTool, gongCreateCallTool, + gongDayByDayActivityTool, gongGetCallTool, gongGetCallTranscriptTool, gongGetCoachingTool, @@ -2763,10 +2778,14 @@ import { } from '@/tools/sentry' import { serperSearchTool } from '@/tools/serper' import { + servicenowAggregateTool, servicenowCreateRecordTool, servicenowDeleteRecordTool, + servicenowDownloadAttachmentTool, + servicenowListAttachmentsTool, servicenowReadRecordTool, servicenowUpdateRecordTool, + servicenowUploadAttachmentTool, } from '@/tools/servicenow' import { sesCreateTemplateTool, @@ -3612,8 +3631,10 @@ export const tools: Record = { fireflies_list_bites: firefliesListBitesTool, fireflies_list_contacts: firefliesListContactsTool, gong_aggregate_activity: gongAggregateActivityTool, + gong_aggregate_by_period: gongAggregateByPeriodTool, gong_answered_scorecards: gongAnsweredScorecardsTool, gong_create_call: gongCreateCallTool, + gong_day_by_day_activity: gongDayByDayActivityTool, gong_get_call: gongGetCallTool, gong_get_call_transcript: gongGetCallTranscriptTool, gong_get_coaching: gongGetCoachingTool, @@ -3991,6 +4012,10 @@ export const tools: Record = { servicenow_read_record: servicenowReadRecordTool, servicenow_update_record: servicenowUpdateRecordTool, servicenow_delete_record: servicenowDeleteRecordTool, + servicenow_aggregate: servicenowAggregateTool, + servicenow_list_attachments: servicenowListAttachmentsTool, + servicenow_download_attachment: servicenowDownloadAttachmentTool, + servicenow_upload_attachment: servicenowUploadAttachmentTool, sixtyfour_find_phone: sixtyfourFindPhoneTool, sixtyfour_find_email: sixtyfourFindEmailTool, sixtyfour_enrich_lead: sixtyfourEnrichLeadTool, @@ -4321,16 +4346,26 @@ export const tools: Record = { dagster_wipe_asset: dagsterWipeAssetTool, databricks_cancel_run: databricksCancelRunTool, databricks_execute_sql: databricksExecuteSqlTool, + databricks_get_cluster: databricksGetClusterTool, + databricks_get_job: databricksGetJobTool, databricks_get_run: databricksGetRunTool, databricks_get_run_output: databricksGetRunOutputTool, + databricks_get_statement: databricksGetStatementTool, databricks_list_clusters: databricksListClustersTool, databricks_list_jobs: databricksListJobsTool, databricks_list_runs: databricksListRunsTool, + databricks_list_warehouses: databricksListWarehousesTool, databricks_run_job: databricksRunJobTool, + dub_bulk_create_links: dubBulkCreateLinksTool, + dub_bulk_delete_links: dubBulkDeleteLinksTool, + dub_bulk_update_links: dubBulkUpdateLinksTool, dub_create_link: dubCreateLinkTool, dub_delete_link: dubDeleteLinkTool, dub_get_analytics: dubGetAnalyticsTool, + dub_get_events: dubGetEventsTool, dub_get_link: dubGetLinkTool, + dub_get_links_count: dubGetLinksCountTool, + dub_get_qr_code: dubGetQrCodeTool, dub_list_links: dubListLinksTool, dub_update_link: dubUpdateLinkTool, dub_upsert_link: dubUpsertLinkTool, @@ -4626,11 +4661,14 @@ export const tools: Record = { enrich_search_company: enrichSearchCompanyTool, enrich_search_company_activities: enrichSearchCompanyActivitiesTool, enrich_search_company_employees: enrichSearchCompanyEmployeesTool, + enrich_search_jobs: enrichSearchJobsTool, enrich_search_logo: enrichSearchLogoTool, enrich_search_people: enrichSearchPeopleTool, enrich_search_people_activities: enrichSearchPeopleActivitiesTool, enrich_search_post_comments: enrichSearchPostCommentsTool, + enrich_search_post_comments_by_url: enrichSearchPostCommentsByUrlTool, enrich_search_post_reactions: enrichSearchPostReactionsTool, + enrich_search_post_reactions_by_url: enrichSearchPostReactionsByUrlTool, enrich_search_posts: enrichSearchPostsTool, enrich_search_similar_companies: enrichSearchSimilarCompaniesTool, enrich_verify_email: enrichVerifyEmailTool, diff --git a/apps/sim/tools/servicenow/aggregate.ts b/apps/sim/tools/servicenow/aggregate.ts new file mode 100644 index 00000000000..551c5a93e4d --- /dev/null +++ b/apps/sim/tools/servicenow/aggregate.ts @@ -0,0 +1,199 @@ +import { createLogger } from '@sim/logger' +import type { + ServiceNowAggregateParams, + ServiceNowAggregateResponse, +} from '@/tools/servicenow/types' +import { createBasicAuthHeader } from '@/tools/servicenow/utils' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('ServiceNowAggregateTool') + +export const aggregateTool: ToolConfig = { + id: 'servicenow_aggregate', + name: 'Aggregate ServiceNow Records', + description: + 'Compute aggregate statistics (count, sum, average, min, max, group by) over a ServiceNow table', + version: '1.0.0', + + params: { + instanceUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'ServiceNow instance URL (e.g., https://instance.service-now.com)', + }, + username: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'ServiceNow username', + }, + password: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'ServiceNow password', + }, + tableName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Table name (e.g., incident, change_request, task)', + }, + query: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Encoded query string to filter records before aggregating (e.g., "active=true")', + }, + count: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Return the count of matching records', + }, + groupBy: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Comma-separated fields to group results by (e.g., category,priority)', + }, + avgFields: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Comma-separated numeric fields to average (e.g., reassignment_count)', + }, + sumFields: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Comma-separated numeric fields to sum', + }, + minFields: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Comma-separated fields to compute the minimum of', + }, + maxFields: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Comma-separated fields to compute the maximum of', + }, + having: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter on aggregate results (e.g., "count>5")', + }, + displayValue: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Return display values for grouped reference fields: "true", "false", or "all"', + }, + }, + + request: { + url: (params) => { + const baseUrl = params.instanceUrl.trim().replace(/\/$/, '') + if (!baseUrl) { + throw new Error('ServiceNow instance URL is required') + } + const url = `${baseUrl}/api/now/stats/${params.tableName.trim()}` + + const queryParams = new URLSearchParams() + + if (params.query) { + queryParams.append('sysparm_query', params.query) + } + if (params.count) { + queryParams.append('sysparm_count', 'true') + } + if (params.groupBy) { + queryParams.append('sysparm_group_by', params.groupBy) + } + if (params.avgFields) { + queryParams.append('sysparm_avg_fields', params.avgFields) + } + if (params.sumFields) { + queryParams.append('sysparm_sum_fields', params.sumFields) + } + if (params.minFields) { + queryParams.append('sysparm_min_fields', params.minFields) + } + if (params.maxFields) { + queryParams.append('sysparm_max_fields', params.maxFields) + } + if (params.having) { + queryParams.append('sysparm_having', params.having) + } + if (params.displayValue) { + queryParams.append('sysparm_display_value', params.displayValue) + } + + const queryString = queryParams.toString() + return queryString ? `${url}?${queryString}` : url + }, + method: 'GET', + headers: (params) => { + if (!params.username || !params.password) { + throw new Error('ServiceNow username and password are required') + } + return { + Authorization: createBasicAuthHeader(params.username, params.password), + Accept: 'application/json', + } + }, + }, + + transformResponse: async (response: Response) => { + try { + const data = await response.json() + + if (!response.ok) { + const error = data.error || data + throw new Error(typeof error === 'string' ? error : error.message || JSON.stringify(error)) + } + + const result = data.result ?? null + const grouped = Array.isArray(result) + const count = !grouped && result?.stats?.count != null ? Number(result.stats.count) : null + + return { + success: true, + output: { + result, + count, + metadata: { + grouped, + groupCount: grouped ? result.length : null, + }, + }, + } + } catch (error) { + logger.error('ServiceNow aggregate - Error processing response:', { error }) + throw error + } + }, + + outputs: { + result: { + type: 'json', + description: + 'Aggregate result. Ungrouped: {stats: {count, sum, avg, min, max}}. Grouped: array of {stats, groupby_fields}.', + }, + count: { + type: 'number', + description: 'Total matching record count (only present for ungrouped count queries)', + optional: true, + }, + metadata: { + type: 'json', + description: 'Operation metadata (grouped, groupCount)', + }, + }, +} diff --git a/apps/sim/tools/servicenow/create_record.ts b/apps/sim/tools/servicenow/create_record.ts index 80f8184f061..477f24b4860 100644 --- a/apps/sim/tools/servicenow/create_record.ts +++ b/apps/sim/tools/servicenow/create_record.ts @@ -47,11 +47,11 @@ export const createRecordTool: ToolConfig { - const baseUrl = params.instanceUrl.replace(/\/$/, '') + const baseUrl = params.instanceUrl.trim().replace(/\/$/, '') if (!baseUrl) { throw new Error('ServiceNow instance URL is required') } - return `${baseUrl}/api/now/table/${params.tableName}` + return `${baseUrl}/api/now/table/${params.tableName.trim()}` }, method: 'POST', headers: (params) => { diff --git a/apps/sim/tools/servicenow/delete_record.ts b/apps/sim/tools/servicenow/delete_record.ts index fdfa6562ede..b92974e9bec 100644 --- a/apps/sim/tools/servicenow/delete_record.ts +++ b/apps/sim/tools/servicenow/delete_record.ts @@ -46,11 +46,11 @@ export const deleteRecordTool: ToolConfig { - const baseUrl = params.instanceUrl.replace(/\/$/, '') + const baseUrl = params.instanceUrl.trim().replace(/\/$/, '') if (!baseUrl) { throw new Error('ServiceNow instance URL is required') } - return `${baseUrl}/api/now/table/${params.tableName}/${params.sysId}` + return `${baseUrl}/api/now/table/${params.tableName.trim()}/${params.sysId.trim()}` }, method: 'DELETE', headers: (params) => { diff --git a/apps/sim/tools/servicenow/download_attachment.ts b/apps/sim/tools/servicenow/download_attachment.ts new file mode 100644 index 00000000000..dc65014550b --- /dev/null +++ b/apps/sim/tools/servicenow/download_attachment.ts @@ -0,0 +1,115 @@ +import { createLogger } from '@sim/logger' +import type { + ServiceNowDownloadAttachmentParams, + ServiceNowDownloadAttachmentResponse, +} from '@/tools/servicenow/types' +import { createBasicAuthHeader } from '@/tools/servicenow/utils' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('ServiceNowDownloadAttachmentTool') + +export const downloadAttachmentTool: ToolConfig< + ServiceNowDownloadAttachmentParams, + ServiceNowDownloadAttachmentResponse +> = { + id: 'servicenow_download_attachment', + name: 'Download ServiceNow Attachment', + description: 'Download an attachment file from ServiceNow by its sys_id', + version: '1.0.0', + + params: { + instanceUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'ServiceNow instance URL (e.g., https://instance.service-now.com)', + }, + username: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'ServiceNow username', + }, + password: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'ServiceNow password', + }, + attachmentSysId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'sys_id of the attachment to download (from List Attachments)', + }, + }, + + request: { + url: (params) => { + const baseUrl = params.instanceUrl.trim().replace(/\/$/, '') + if (!baseUrl) { + throw new Error('ServiceNow instance URL is required') + } + return `${baseUrl}/api/now/attachment/${params.attachmentSysId.trim()}/file` + }, + method: 'GET', + headers: (params) => { + if (!params.username || !params.password) { + throw new Error('ServiceNow username and password are required') + } + return { + Authorization: createBasicAuthHeader(params.username, params.password), + Accept: '*/*', + } + }, + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const errorText = await response.text() + logger.error('ServiceNow download attachment - request failed', { + status: response.status, + errorText, + }) + throw new Error(errorText || `Failed to download attachment: ${response.status}`) + } + + const contentType = response.headers.get('content-type') || 'application/octet-stream' + const contentDisposition = response.headers.get('content-disposition') + let fileName = 'attachment' + + if (contentDisposition) { + const match = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/) + if (match?.[1]) { + fileName = match[1].replace(/['"]/g, '') + } + } + + const arrayBuffer = await response.arrayBuffer() + const buffer = Buffer.from(arrayBuffer) + + return { + success: true, + output: { + file: { + name: fileName, + mimeType: contentType, + data: buffer.toString('base64'), + size: buffer.length, + }, + content: buffer.toString('base64'), + }, + } + }, + + outputs: { + file: { + type: 'file', + description: 'Downloaded attachment stored in execution files', + }, + content: { + type: 'string', + description: 'Base64 encoded file content', + }, + }, +} diff --git a/apps/sim/tools/servicenow/index.ts b/apps/sim/tools/servicenow/index.ts index 905b22d8a83..81f483a4f2f 100644 --- a/apps/sim/tools/servicenow/index.ts +++ b/apps/sim/tools/servicenow/index.ts @@ -1,11 +1,19 @@ +import { aggregateTool } from '@/tools/servicenow/aggregate' import { createRecordTool } from '@/tools/servicenow/create_record' import { deleteRecordTool } from '@/tools/servicenow/delete_record' +import { downloadAttachmentTool } from '@/tools/servicenow/download_attachment' +import { listAttachmentsTool } from '@/tools/servicenow/list_attachments' import { readRecordTool } from '@/tools/servicenow/read_record' import { updateRecordTool } from '@/tools/servicenow/update_record' +import { uploadAttachmentTool } from '@/tools/servicenow/upload_attachment' export { createRecordTool as servicenowCreateRecordTool, readRecordTool as servicenowReadRecordTool, updateRecordTool as servicenowUpdateRecordTool, deleteRecordTool as servicenowDeleteRecordTool, + aggregateTool as servicenowAggregateTool, + listAttachmentsTool as servicenowListAttachmentsTool, + downloadAttachmentTool as servicenowDownloadAttachmentTool, + uploadAttachmentTool as servicenowUploadAttachmentTool, } diff --git a/apps/sim/tools/servicenow/list_attachments.ts b/apps/sim/tools/servicenow/list_attachments.ts new file mode 100644 index 00000000000..c35bd08afe8 --- /dev/null +++ b/apps/sim/tools/servicenow/list_attachments.ts @@ -0,0 +1,135 @@ +import { createLogger } from '@sim/logger' +import type { + ServiceNowListAttachmentsParams, + ServiceNowListAttachmentsResponse, +} from '@/tools/servicenow/types' +import { createBasicAuthHeader } from '@/tools/servicenow/utils' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('ServiceNowListAttachmentsTool') + +export const listAttachmentsTool: ToolConfig< + ServiceNowListAttachmentsParams, + ServiceNowListAttachmentsResponse +> = { + id: 'servicenow_list_attachments', + name: 'List ServiceNow Attachments', + description: 'List the attachments on a ServiceNow record', + version: '1.0.0', + + params: { + instanceUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'ServiceNow instance URL (e.g., https://instance.service-now.com)', + }, + username: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'ServiceNow username', + }, + password: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'ServiceNow password', + }, + tableName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Table that owns the record (e.g., incident, change_request)', + }, + recordSysId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'sys_id of the record whose attachments should be listed', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of attachments to return', + }, + }, + + request: { + url: (params) => { + const baseUrl = params.instanceUrl.trim().replace(/\/$/, '') + if (!baseUrl) { + throw new Error('ServiceNow instance URL is required') + } + + const queryParams = new URLSearchParams() + queryParams.append( + 'sysparm_query', + `table_name=${params.tableName.trim()}^table_sys_id=${params.recordSysId.trim()}` + ) + if (params.limit) { + queryParams.append('sysparm_limit', params.limit.toString()) + } + + return `${baseUrl}/api/now/attachment?${queryParams.toString()}` + }, + method: 'GET', + headers: (params) => { + if (!params.username || !params.password) { + throw new Error('ServiceNow username and password are required') + } + return { + Authorization: createBasicAuthHeader(params.username, params.password), + Accept: 'application/json', + } + }, + }, + + transformResponse: async (response: Response) => { + try { + const data = await response.json() + + if (!response.ok) { + const error = data.error || data + throw new Error(typeof error === 'string' ? error : error.message || JSON.stringify(error)) + } + + const attachments = Array.isArray(data.result) ? data.result : [] + + return { + success: true, + output: { + attachments, + metadata: { + recordCount: attachments.length, + }, + }, + } + } catch (error) { + logger.error('ServiceNow list attachments - Error processing response:', { error }) + throw error + } + }, + + outputs: { + attachments: { + type: 'array', + description: 'Attachment metadata records', + items: { + type: 'object', + properties: { + sys_id: { type: 'string', description: 'Attachment sys_id' }, + file_name: { type: 'string', description: 'File name' }, + content_type: { type: 'string', description: 'MIME type' }, + size_bytes: { type: 'string', description: 'File size in bytes' }, + download_link: { type: 'string', description: 'Direct download URL for the file' }, + }, + }, + }, + metadata: { + type: 'json', + description: 'Operation metadata (recordCount)', + }, + }, +} diff --git a/apps/sim/tools/servicenow/read_record.ts b/apps/sim/tools/servicenow/read_record.ts index 347c7c0c6c7..8a9497b3476 100644 --- a/apps/sim/tools/servicenow/read_record.ts +++ b/apps/sim/tools/servicenow/read_record.ts @@ -84,16 +84,16 @@ export const readRecordTool: ToolConfig { - const baseUrl = params.instanceUrl.replace(/\/$/, '') + const baseUrl = params.instanceUrl.trim().replace(/\/$/, '') if (!baseUrl) { throw new Error('ServiceNow instance URL is required') } - let url = `${baseUrl}/api/now/table/${params.tableName}` + let url = `${baseUrl}/api/now/table/${params.tableName.trim()}` const queryParams = new URLSearchParams() if (params.sysId) { - url = `${url}/${params.sysId}` + url = `${url}/${params.sysId.trim()}` } else if (params.number) { const numberQuery = `number=${params.number}` const existingQuery = params.query diff --git a/apps/sim/tools/servicenow/types.ts b/apps/sim/tools/servicenow/types.ts index 25fbba30065..618664e5470 100644 --- a/apps/sim/tools/servicenow/types.ts +++ b/apps/sim/tools/servicenow/types.ts @@ -73,8 +73,103 @@ export interface ServiceNowDeleteResponse extends ToolResponse { } } +export interface ServiceNowAggregateParams extends ServiceNowBaseParams { + query?: string + count?: boolean + groupBy?: string + avgFields?: string + sumFields?: string + minFields?: string + maxFields?: string + having?: string + displayValue?: string +} + +export interface ServiceNowAggregateResponse extends ToolResponse { + output: { + result: Record | Record[] | null + count: number | null + metadata: { + grouped: boolean + groupCount: number | null + } + } +} + +interface ServiceNowAttachment { + sys_id: string + file_name: string + content_type: string + size_bytes?: string + table_name?: string + table_sys_id?: string + download_link?: string + [key: string]: any +} + +export interface ServiceNowListAttachmentsParams { + instanceUrl: string + username: string + password: string + tableName: string + recordSysId: string + limit?: number +} + +export interface ServiceNowListAttachmentsResponse extends ToolResponse { + output: { + attachments: ServiceNowAttachment[] + metadata: { + recordCount: number + } + } +} + +export interface ServiceNowDownloadAttachmentParams { + instanceUrl: string + username: string + password: string + attachmentSysId: string +} + +export interface ServiceNowDownloadAttachmentResponse extends ToolResponse { + output: { + file: { + name: string + mimeType: string + data: string + size: number + } + content: string + } +} + +export interface ServiceNowUploadAttachmentParams { + instanceUrl: string + username: string + password: string + tableName: string + recordSysId: string + fileName: string + file?: unknown + fileContent?: string +} + +export interface ServiceNowUploadAttachmentResponse extends ToolResponse { + output: { + attachment: ServiceNowAttachment + metadata: { + recordCount: 1 + } + } +} + export type ServiceNowResponse = | ServiceNowCreateResponse | ServiceNowReadResponse | ServiceNowUpdateResponse | ServiceNowDeleteResponse + | ServiceNowAggregateResponse + | ServiceNowListAttachmentsResponse + | ServiceNowDownloadAttachmentResponse + | ServiceNowUploadAttachmentResponse diff --git a/apps/sim/tools/servicenow/update_record.ts b/apps/sim/tools/servicenow/update_record.ts index b9dcd148a7c..709ddfc9475 100644 --- a/apps/sim/tools/servicenow/update_record.ts +++ b/apps/sim/tools/servicenow/update_record.ts @@ -52,11 +52,11 @@ export const updateRecordTool: ToolConfig { - const baseUrl = params.instanceUrl.replace(/\/$/, '') + const baseUrl = params.instanceUrl.trim().replace(/\/$/, '') if (!baseUrl) { throw new Error('ServiceNow instance URL is required') } - return `${baseUrl}/api/now/table/${params.tableName}/${params.sysId}` + return `${baseUrl}/api/now/table/${params.tableName.trim()}/${params.sysId.trim()}` }, method: 'PATCH', headers: (params) => { diff --git a/apps/sim/tools/servicenow/upload_attachment.ts b/apps/sim/tools/servicenow/upload_attachment.ts new file mode 100644 index 00000000000..f9907476d6f --- /dev/null +++ b/apps/sim/tools/servicenow/upload_attachment.ts @@ -0,0 +1,112 @@ +import { createLogger } from '@sim/logger' +import type { + ServiceNowUploadAttachmentParams, + ServiceNowUploadAttachmentResponse, +} from '@/tools/servicenow/types' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('ServiceNowUploadAttachmentTool') + +export const uploadAttachmentTool: ToolConfig< + ServiceNowUploadAttachmentParams, + ServiceNowUploadAttachmentResponse +> = { + id: 'servicenow_upload_attachment', + name: 'Upload ServiceNow Attachment', + description: 'Attach a file to a ServiceNow record', + version: '1.0.0', + + params: { + instanceUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'ServiceNow instance URL (e.g., https://instance.service-now.com)', + }, + username: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'ServiceNow username', + }, + password: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'ServiceNow password', + }, + tableName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Table that owns the record (e.g., incident, change_request)', + }, + recordSysId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'sys_id of the record to attach the file to', + }, + fileName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Name to give the uploaded file (e.g., logs.txt)', + }, + file: { + type: 'file', + required: false, + visibility: 'user-only', + description: 'File to upload (UserFile object)', + }, + fileContent: { + type: 'string', + required: false, + visibility: 'hidden', + description: 'Base64-encoded file content (legacy)', + }, + }, + + request: { + url: '/api/tools/servicenow/upload-attachment', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => ({ + instanceUrl: params.instanceUrl, + username: params.username, + password: params.password, + tableName: params.tableName, + recordSysId: params.recordSysId, + fileName: params.fileName, + file: params.file, + fileContent: params.fileContent, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!data.success) { + logger.error('ServiceNow upload attachment failed', { error: data.error }) + throw new Error(data.error || 'ServiceNow upload attachment failed') + } + + return { + success: true, + output: data.output, + } + }, + + outputs: { + attachment: { + type: 'json', + description: 'Created attachment metadata (sys_id, file_name, content_type, download_link)', + }, + metadata: { + type: 'json', + description: 'Operation metadata', + }, + }, +} diff --git a/apps/sim/tools/workday/types.ts b/apps/sim/tools/workday/types.ts index 574287dbe37..b2ad178e721 100644 --- a/apps/sim/tools/workday/types.ts +++ b/apps/sim/tools/workday/types.ts @@ -8,15 +8,12 @@ interface WorkdayBaseParams { } interface WorkdayWorker { - id: string - descriptor: string - primaryWorkEmail?: string - primaryWorkPhone?: string - businessTitle?: string - supervisoryOrganization?: string - hireDate?: string - workerType?: string - isActive?: boolean + id: string | null + descriptor: string | null + personalData?: unknown + employmentData?: unknown + compensationData?: unknown + organizationData?: unknown [key: string]: unknown } From 5beb97132b5f1f759023b3d1cd314f55e90829ff Mon Sep 17 00:00:00 2001 From: waleed Date: Tue, 9 Jun 2026 20:17:53 -0700 Subject: [PATCH 2/7] fix(servicenow): give list-attachments limit a unique subBlock id Read Records and List Attachments shared the subBlock id 'limit', so the single-value-per-id store could bleed the value across operations. Rename the new list-attachments field to attachmentLimit and map it back to the tool's limit param. --- apps/sim/blocks/blocks/servicenow.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/apps/sim/blocks/blocks/servicenow.ts b/apps/sim/blocks/blocks/servicenow.ts index 3a6c23dacc2..6dea44bd91d 100644 --- a/apps/sim/blocks/blocks/servicenow.ts +++ b/apps/sim/blocks/blocks/servicenow.ts @@ -314,9 +314,9 @@ Output: {"state": "2", "assigned_to": "john.doe", "work_notes": "Assigned and st required: true, description: 'sys_id of the record the attachment belongs to', }, - // List attachments: limit + // List attachments: limit (unique id to avoid sharing the Read Records `limit` value) { - id: 'limit', + id: 'attachmentLimit', title: 'Limit', type: 'short-input', placeholder: '10', @@ -385,10 +385,11 @@ Output: {"state": "2", "assigned_to": "john.doe", "work_notes": "Assigned and st config: { tool: (params) => params.operation, params: (params) => { - const { operation, fields, file, ...rest } = params + const { operation, fields, file, attachmentLimit, ...rest } = params const isCreateOrUpdate = operation === 'servicenow_create_record' || operation === 'servicenow_update_record' + if (attachmentLimit != null && attachmentLimit !== '') rest.limit = Number(attachmentLimit) if (rest.limit != null && rest.limit !== '') rest.limit = Number(rest.limit) if (rest.offset != null && rest.offset !== '') rest.offset = Number(rest.offset) @@ -423,6 +424,10 @@ Output: {"state": "2", "assigned_to": "john.doe", "work_notes": "Assigned and st number: { type: 'string', description: 'Record number' }, query: { type: 'string', description: 'Query string' }, limit: { type: 'number', description: 'Result limit' }, + attachmentLimit: { + type: 'number', + description: 'Max attachments to return (list attachments)', + }, offset: { type: 'number', description: 'Pagination offset' }, fields: { type: 'json', description: 'Fields object or JSON string' }, displayValue: { type: 'string', description: 'Display value mode for reference fields' }, From 584c5ca7708dc509a8ea4169ed381800b4ecee69 Mon Sep 17 00:00:00 2001 From: waleed Date: Tue, 9 Jun 2026 20:19:54 -0700 Subject: [PATCH 3/7] fix(servicenow): type upload-attachment response json to fix build typecheck secureFetchWithValidation's response.json() resolves to unknown under the build's stricter typecheck; cast the parsed body so data.result is accessible. --- apps/sim/app/api/tools/servicenow/upload-attachment/route.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/sim/app/api/tools/servicenow/upload-attachment/route.ts b/apps/sim/app/api/tools/servicenow/upload-attachment/route.ts index da0967dfcd9..e105359bd99 100644 --- a/apps/sim/app/api/tools/servicenow/upload-attachment/route.ts +++ b/apps/sim/app/api/tools/servicenow/upload-attachment/route.ts @@ -97,7 +97,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => { return NextResponse.json({ success: false, error: errorMessage }, { status: response.status }) } - const data = await response.json() + const data = (await response.json()) as { result?: unknown } logger.info(`[${requestId}] File attached to ServiceNow record successfully`, { tableName: body.tableName, From 4615046867e2979510d676eb205ea156a6c37a40 Mon Sep 17 00:00:00 2001 From: waleed Date: Tue, 9 Jun 2026 20:22:21 -0700 Subject: [PATCH 4/7] refactor(servicenow): type upload-attachment response per file-route convention Match the SharePoint/OneDrive upload pattern: import the named ServiceNowAttachment type from the tool's types, cast response.json() to it, and extract specific fields with ?? null instead of passing data.result through as an opaque unknown blob. --- .../tools/servicenow/upload-attachment/route.ts | 16 ++++++++++++++-- apps/sim/tools/servicenow/types.ts | 2 +- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/apps/sim/app/api/tools/servicenow/upload-attachment/route.ts b/apps/sim/app/api/tools/servicenow/upload-attachment/route.ts index e105359bd99..af0abb9068f 100644 --- a/apps/sim/app/api/tools/servicenow/upload-attachment/route.ts +++ b/apps/sim/app/api/tools/servicenow/upload-attachment/route.ts @@ -10,6 +10,7 @@ import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { processSingleFileToUserFile } from '@/lib/uploads/utils/file-utils' import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server' import { assertToolFileAccess } from '@/app/api/files/authorization' +import type { ServiceNowAttachment } from '@/tools/servicenow/types' import { createBasicAuthHeader } from '@/tools/servicenow/utils' export const dynamic = 'force-dynamic' @@ -97,7 +98,8 @@ export const POST = withRouteHandler(async (request: NextRequest) => { return NextResponse.json({ success: false, error: errorMessage }, { status: response.status }) } - const data = (await response.json()) as { result?: unknown } + const data = (await response.json()) as { result?: ServiceNowAttachment } + const result = data.result logger.info(`[${requestId}] File attached to ServiceNow record successfully`, { tableName: body.tableName, @@ -107,7 +109,17 @@ export const POST = withRouteHandler(async (request: NextRequest) => { return NextResponse.json({ success: true, output: { - attachment: data.result, + attachment: result + ? { + sys_id: result.sys_id ?? null, + file_name: result.file_name ?? null, + content_type: result.content_type ?? null, + size_bytes: result.size_bytes ?? null, + table_name: result.table_name ?? null, + table_sys_id: result.table_sys_id ?? null, + download_link: result.download_link ?? null, + } + : null, metadata: { recordCount: 1 }, }, }) diff --git a/apps/sim/tools/servicenow/types.ts b/apps/sim/tools/servicenow/types.ts index 618664e5470..43c6a9dcccc 100644 --- a/apps/sim/tools/servicenow/types.ts +++ b/apps/sim/tools/servicenow/types.ts @@ -96,7 +96,7 @@ export interface ServiceNowAggregateResponse extends ToolResponse { } } -interface ServiceNowAttachment { +export interface ServiceNowAttachment { sys_id: string file_name: string content_type: string From ff42c41cdf4fd08f15afcda0ae475a625a9049f6 Mon Sep 17 00:00:00 2001 From: waleed Date: Tue, 9 Jun 2026 20:29:49 -0700 Subject: [PATCH 5/7] fix(servicenow): remove invalid 'custom' generationType from groupBy wandConfig 'custom' is not a member of the GenerationType union, breaking the build typecheck. No valid type fits a comma-separated field list, so drop the wandConfig (consistent with the block's other field-list inputs). --- apps/sim/blocks/blocks/servicenow.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/apps/sim/blocks/blocks/servicenow.ts b/apps/sim/blocks/blocks/servicenow.ts index 6dea44bd91d..c2531759b1c 100644 --- a/apps/sim/blocks/blocks/servicenow.ts +++ b/apps/sim/blocks/blocks/servicenow.ts @@ -248,13 +248,6 @@ Output: {"state": "2", "assigned_to": "john.doe", "work_notes": "Assigned and st placeholder: 'category,priority', condition: { field: 'operation', value: 'servicenow_aggregate' }, description: 'Comma-separated fields to group results by', - wandConfig: { - enabled: true, - prompt: - 'Generate a comma-separated list of ServiceNow field names to group aggregate results by, based on the user request. Use lowercase field names. Return ONLY the comma-separated field names - no explanations, no extra text.', - placeholder: 'Describe how to group the results...', - generationType: 'custom', - }, }, { id: 'avgFields', From 86e259df249b8070b4cd4f1ff212fefe707ca4ab Mon Sep 17 00:00:00 2001 From: waleed Date: Tue, 9 Jun 2026 20:37:24 -0700 Subject: [PATCH 6/7] fix(servicenow): drop redundant hidden fileContent param from upload attachment Per project rule, visibility:'hidden' is reserved for framework-injected tokens, not user-supplied data. fileContent was a copied-over legacy fallback with no caller (the block uploads via the canonical file/UserFile path), so remove it from the tool, types, contract, and route. --- .../servicenow/upload-attachment/route.ts | 36 ++++++++----------- .../sim/lib/api/contracts/tools/servicenow.ts | 1 - apps/sim/tools/servicenow/types.ts | 1 - .../sim/tools/servicenow/upload_attachment.ts | 7 ---- 4 files changed, 14 insertions(+), 31 deletions(-) diff --git a/apps/sim/app/api/tools/servicenow/upload-attachment/route.ts b/apps/sim/app/api/tools/servicenow/upload-attachment/route.ts index af0abb9068f..4ddd8d162fb 100644 --- a/apps/sim/app/api/tools/servicenow/upload-attachment/route.ts +++ b/apps/sim/app/api/tools/servicenow/upload-attachment/route.ts @@ -35,34 +35,26 @@ export const POST = withRouteHandler(async (request: NextRequest) => { if (!parsed.success) return parsed.response const body = parsed.data.body - let fileBuffer: Buffer - let contentType = 'application/octet-stream' - - if (body.file) { - let userFile - try { - userFile = processSingleFileToUserFile(body.file, requestId, logger) - } catch (error) { - return NextResponse.json( - { success: false, error: getErrorMessage(error, 'Failed to process file') }, - { status: 400 } - ) - } - - const denied = await assertToolFileAccess(userFile.key, authResult.userId, requestId, logger) - if (denied) return denied + if (!body.file) { + return NextResponse.json({ success: false, error: 'A file is required' }, { status: 400 }) + } - if (userFile.type) contentType = userFile.type - fileBuffer = await downloadFileFromStorage(userFile, requestId, logger) - } else if (body.fileContent) { - fileBuffer = Buffer.from(body.fileContent, 'base64') - } else { + let userFile + try { + userFile = processSingleFileToUserFile(body.file, requestId, logger) + } catch (error) { return NextResponse.json( - { success: false, error: 'Either file or fileContent must be provided' }, + { success: false, error: getErrorMessage(error, 'Failed to process file') }, { status: 400 } ) } + const denied = await assertToolFileAccess(userFile.key, authResult.userId, requestId, logger) + if (denied) return denied + + const contentType = userFile.type || 'application/octet-stream' + const fileBuffer = await downloadFileFromStorage(userFile, requestId, logger) + const baseUrl = body.instanceUrl.trim().replace(/\/$/, '') const uploadParams = new URLSearchParams({ table_name: body.tableName.trim(), diff --git a/apps/sim/lib/api/contracts/tools/servicenow.ts b/apps/sim/lib/api/contracts/tools/servicenow.ts index cab00d7ae01..49ca5e545dc 100644 --- a/apps/sim/lib/api/contracts/tools/servicenow.ts +++ b/apps/sim/lib/api/contracts/tools/servicenow.ts @@ -10,7 +10,6 @@ export const servicenowUploadAttachmentBodySchema = z.object({ recordSysId: z.string().min(1, 'Record sys_id is required'), fileName: z.string().min(1, 'File name is required'), file: RawFileInputSchema.optional().nullable(), - fileContent: z.string().optional().nullable(), }) export type ServiceNowUploadAttachmentBody = z.input diff --git a/apps/sim/tools/servicenow/types.ts b/apps/sim/tools/servicenow/types.ts index 43c6a9dcccc..f86012609be 100644 --- a/apps/sim/tools/servicenow/types.ts +++ b/apps/sim/tools/servicenow/types.ts @@ -152,7 +152,6 @@ export interface ServiceNowUploadAttachmentParams { recordSysId: string fileName: string file?: unknown - fileContent?: string } export interface ServiceNowUploadAttachmentResponse extends ToolResponse { diff --git a/apps/sim/tools/servicenow/upload_attachment.ts b/apps/sim/tools/servicenow/upload_attachment.ts index f9907476d6f..1fb54837c81 100644 --- a/apps/sim/tools/servicenow/upload_attachment.ts +++ b/apps/sim/tools/servicenow/upload_attachment.ts @@ -59,12 +59,6 @@ export const uploadAttachmentTool: ToolConfig< visibility: 'user-only', description: 'File to upload (UserFile object)', }, - fileContent: { - type: 'string', - required: false, - visibility: 'hidden', - description: 'Base64-encoded file content (legacy)', - }, }, request: { @@ -81,7 +75,6 @@ export const uploadAttachmentTool: ToolConfig< recordSysId: params.recordSysId, fileName: params.fileName, file: params.file, - fileContent: params.fileContent, }), }, From e5cd4f849999cb7bb45702084bff41ab38f71cda Mon Sep 17 00:00:00 2001 From: waleed Date: Tue, 9 Jun 2026 20:38:16 -0700 Subject: [PATCH 7/7] fix(dub): require a link selector for bulk update links Bulk Update Links could run with only Update Data and no Link IDs or External IDs, sending Dub a request with no target links and producing a confusing API error. Guard the tool to throw a clear validation error when neither selector is provided, and clarify the block placeholder. --- apps/sim/blocks/blocks/dub.ts | 2 +- apps/sim/tools/dub/bulk_update_links.ts | 25 +++++++++++++++++++++---- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/apps/sim/blocks/blocks/dub.ts b/apps/sim/blocks/blocks/dub.ts index f2fa46ea26e..ff68528bdc3 100644 --- a/apps/sim/blocks/blocks/dub.ts +++ b/apps/sim/blocks/blocks/dub.ts @@ -481,7 +481,7 @@ export const DubBlock: BlockConfig = { id: 'bulkUpdateLinkIds', title: 'Link IDs', type: 'short-input', - placeholder: 'Comma-separated link IDs (max 100)', + placeholder: 'Comma-separated link IDs (required unless External IDs is set)', condition: { field: 'operation', value: 'bulk_update_links' }, }, { diff --git a/apps/sim/tools/dub/bulk_update_links.ts b/apps/sim/tools/dub/bulk_update_links.ts index bab05c3f052..f4923ed8420 100644 --- a/apps/sim/tools/dub/bulk_update_links.ts +++ b/apps/sim/tools/dub/bulk_update_links.ts @@ -47,11 +47,28 @@ export const bulkUpdateLinksTool: ToolConfig { const data = typeof params.data === 'string' ? JSON.parse(params.data) : params.data + const linkIds = params.linkIds + ? params.linkIds + .split(',') + .map((id) => id.trim()) + .filter(Boolean) + : [] + const externalIds = params.externalIds + ? params.externalIds + .split(',') + .map((id) => id.trim()) + .filter(Boolean) + : [] + if (linkIds.length === 0 && externalIds.length === 0) { + throw new Error( + 'Bulk Update Links requires at least one Link ID or External ID to select which links to update.' + ) + } const body: Record = { data: data ?? {} } - if (params.linkIds) { - body.linkIds = params.linkIds.split(',').map((id) => id.trim()) - } else if (params.externalIds) { - body.externalIds = params.externalIds.split(',').map((id) => id.trim()) + if (linkIds.length > 0) { + body.linkIds = linkIds + } else { + body.externalIds = externalIds } return body },