diff --git a/apps/docs/components/icons.tsx b/apps/docs/components/icons.tsx index 725bad45557..2240f766529 100644 --- a/apps/docs/components/icons.tsx +++ b/apps/docs/components/icons.tsx @@ -30,29 +30,44 @@ export function AgentMailIcon(props: SVGProps) { export function AgentPhoneIcon(props: SVGProps) { return ( - + + + + ) @@ -536,7 +551,7 @@ export function HubspotIcon(props: SVGProps) { xmlns='http://www.w3.org/2000/svg' fill='currentColor' > - + ) } @@ -2296,23 +2311,26 @@ export function ElevenLabsIcon(props: SVGProps) { } export function FindymailIcon(props: SVGProps) { + const id = useId() + const gradient0 = `findymail_paint0_${id}` + const gradient1 = `findymail_paint1_${id}` return ( ) { ) => ( ) export const ResendIcon = (props: SVGProps) => ( - + ) { ) } +export function LitellmIcon(props: SVGProps) { + return ( + + LiteLLM + + + ) +} + export function PosthogIcon(props: SVGProps) { return ( ) { } export function ZoomIcon(props: SVGProps) { + return ( + + ) +} + +export function ZoomInfoIcon(props: SVGProps) { + const id = useId() + const clipId = `zoominfo-clip_${id}` return ( ) } diff --git a/apps/docs/components/ui/icon-mapping.ts b/apps/docs/components/ui/icon-mapping.ts index 29ff65d4eb1..9aef4ce8241 100644 --- a/apps/docs/components/ui/icon-mapping.ts +++ b/apps/docs/components/ui/icon-mapping.ts @@ -209,6 +209,7 @@ import { ZendeskIcon, ZepIcon, ZoomIcon, + ZoomInfoIcon, } from '@/components/icons' type IconComponent = ComponentType> @@ -448,4 +449,5 @@ export const blockTypeToIconMap: Record = { zendesk: ZendeskIcon, zep: ZepIcon, zoom: ZoomIcon, + zoominfo: ZoomInfoIcon, } diff --git a/apps/docs/content/docs/en/tools/agentmail.mdx b/apps/docs/content/docs/en/tools/agentmail.mdx index 5d5a14c3934..9432049e745 100644 --- a/apps/docs/content/docs/en/tools/agentmail.mdx +++ b/apps/docs/content/docs/en/tools/agentmail.mdx @@ -262,6 +262,8 @@ Get details of a specific email message in AgentMail | `subject` | string | Message subject | | `text` | string | Plain text content | | `html` | string | HTML content | +| `labels` | array | Labels assigned to the message | +| `timestamp` | string | Time the message was sent or drafted | | `createdAt` | string | Creation timestamp | ### `agentmail_get_thread` @@ -298,6 +300,8 @@ Get details of a specific email thread including messages in AgentMail | ↳ `subject` | string | Message subject | | ↳ `text` | string | Plain text content | | ↳ `html` | string | HTML content | +| ↳ `labels` | array | Labels assigned to the message | +| ↳ `timestamp` | string | Time the message was sent or drafted | | ↳ `createdAt` | string | Creation timestamp | ### `agentmail_list_drafts` @@ -380,6 +384,7 @@ List messages in an inbox in AgentMail | ↳ `to` | array | Recipient email addresses | | ↳ `subject` | string | Message subject | | ↳ `preview` | string | Message preview text | +| ↳ `timestamp` | string | Time the message was sent or drafted | | ↳ `createdAt` | string | Creation timestamp | | `count` | number | Total number of messages | | `nextPageToken` | string | Token for retrieving the next page | diff --git a/apps/docs/content/docs/en/tools/agentphone.mdx b/apps/docs/content/docs/en/tools/agentphone.mdx index e70c115ec18..11dea1cd61c 100644 --- a/apps/docs/content/docs/en/tools/agentphone.mdx +++ b/apps/docs/content/docs/en/tools/agentphone.mdx @@ -7,7 +7,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card" {/* MANUAL-CONTENT-START:intro */} @@ -254,6 +254,7 @@ Get a conversation along with its recent messages | ↳ `direction` | string | inbound or outbound | | ↳ `channel` | string | sms, mms, or imessage | | ↳ `mediaUrl` | string | Attached media URL | +| ↳ `mediaUrls` | array | All attached media URLs | | ↳ `receivedAt` | string | ISO 8601 timestamp | ### `agentphone_get_conversation_messages` @@ -282,6 +283,7 @@ Get paginated messages for a conversation | ↳ `direction` | string | inbound or outbound | | ↳ `channel` | string | sms, mms, or imessage | | ↳ `mediaUrl` | string | Attached media URL | +| ↳ `mediaUrls` | array | All attached media URLs | | ↳ `receivedAt` | string | ISO 8601 timestamp | | `hasMore` | boolean | Whether more messages are available | @@ -425,7 +427,7 @@ List contacts for this AgentPhone account | --------- | ---- | -------- | ----------- | | `apiKey` | string | Yes | AgentPhone API key | | `search` | string | No | Filter by name or phone number \(case-insensitive contains\) | -| `limit` | number | No | Number of results to return \(default 50\) | +| `limit` | number | No | Number of results to return \(default 50, max 200\) | | `offset` | number | No | Number of results to skip \(min 0\) | #### Output @@ -624,6 +626,7 @@ Update conversation metadata (stored state). Pass null to clear existing metadat | ↳ `direction` | string | inbound or outbound | | ↳ `channel` | string | Channel \(sms, mms, etc.\) | | ↳ `mediaUrl` | string | Media URL if any | +| ↳ `mediaUrls` | array | All attached media URLs | | ↳ `receivedAt` | string | ISO 8601 timestamp | diff --git a/apps/docs/content/docs/en/tools/apollo.mdx b/apps/docs/content/docs/en/tools/apollo.mdx index 355f62d9642..42216ca5d1c 100644 --- a/apps/docs/content/docs/en/tools/apollo.mdx +++ b/apps/docs/content/docs/en/tools/apollo.mdx @@ -179,7 +179,7 @@ Enrich data for up to 10 organizations at once using Apollo | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `apiKey` | string | Yes | Apollo API key | -| `organizations` | array | Yes | Array of organizations to enrich \(max 10\). Each item requires `name` and may include `domain` \(e.g., \[\{"name": "Example Corp", "domain": "example.com"\}\]\) | +| `domains` | array | Yes | Array of company domains to enrich \(max 10, no www. or @, e.g., \["apollo.io", "stripe.com"\]\) | #### Output @@ -325,8 +325,10 @@ Update up to 100 existing contacts at once in your Apollo database. Each contact | Parameter | Type | Description | | --------- | ---- | ----------- | -| `message` | string | Confirmation message from Apollo | -| `job_id` | string | Async job ID \(returned for >100 contacts\) | +| `contacts` | json | Updated contacts \(synchronous response, ≤100 contacts\) | +| `entity_progress_job` | json | Async job descriptor \(>100 contacts or async=true\): \{id, status, ...\} | +| `job_id` | string | Async job ID extracted from entity_progress_job | +| `message` | string | Optional confirmation message from Apollo | ### `apollo_account_create` @@ -445,8 +447,11 @@ Update up to 1000 existing accounts at once in your Apollo database (higher limi | Parameter | Type | Description | | --------- | ---- | ----------- | -| `message` | string | Confirmation message from Apollo | +| `accounts` | json | Updated accounts \(synchronous response, ≤100 accounts\): \[\{id, account_stage_id, ...\}\] | | `account_ids` | json | IDs of accounts that were updated | +| `entity_progress_job` | json | Async job descriptor \(>100 accounts or async=true\) | +| `job_id` | string | Async job ID extracted from entity_progress_job | +| `message` | string | Optional confirmation message from Apollo | ### `apollo_opportunity_create` diff --git a/apps/docs/content/docs/en/tools/azure_devops.mdx b/apps/docs/content/docs/en/tools/azure_devops.mdx index e0add5348ae..14ee59415fb 100644 --- a/apps/docs/content/docs/en/tools/azure_devops.mdx +++ b/apps/docs/content/docs/en/tools/azure_devops.mdx @@ -373,7 +373,7 @@ Fetch full details of a single work item by ID from Azure DevOps, including titl ### `azure_devops_get_work_items_batch` -Fetch full details for multiple work items by ID from Azure DevOps. Pass comma-separated IDs (e.g. +Fetch full details for multiple work items by ID from Azure DevOps. Pass comma-separated IDs (e.g. #### Input diff --git a/apps/docs/content/docs/en/tools/docusign.mdx b/apps/docs/content/docs/en/tools/docusign.mdx index 3930a121554..253104c3cec 100644 --- a/apps/docs/content/docs/en/tools/docusign.mdx +++ b/apps/docs/content/docs/en/tools/docusign.mdx @@ -171,7 +171,8 @@ Download a signed document from a completed DocuSign envelope | Parameter | Type | Description | | --------- | ---- | ----------- | -| `base64Content` | string | Base64-encoded document content | +| `file` | file | Stored downloaded document file | +| `base64Content` | string | Deprecated legacy inline content. New downloads return file. | | `mimeType` | string | MIME type of the document | | `fileName` | string | Original file name | diff --git a/apps/docs/content/docs/en/tools/elevenlabs.mdx b/apps/docs/content/docs/en/tools/elevenlabs.mdx index 10c952b1250..c028f8813ca 100644 --- a/apps/docs/content/docs/en/tools/elevenlabs.mdx +++ b/apps/docs/content/docs/en/tools/elevenlabs.mdx @@ -1,6 +1,6 @@ --- title: ElevenLabs -description: Convert TTS using ElevenLabs +description: Convert text to speech with ElevenLabs --- import { BlockInfoCard } from "@/components/ui/block-info-card" @@ -35,7 +35,7 @@ Integrate ElevenLabs into the workflow. Can convert text to speech. ### `elevenlabs_tts` -Convert TTS using ElevenLabs voices +Convert text to speech using ElevenLabs voices #### Input @@ -45,7 +45,7 @@ Convert TTS using ElevenLabs voices | `voiceId` | string | Yes | The ID of the voice to use \(e.g., "21m00Tcm4TlvDq8ikWAM" for Rachel\) | | `modelId` | string | No | The ID of the model to use \(e.g., "eleven_multilingual_v2", "eleven_turbo_v2"\). Defaults to eleven_monolingual_v1 | | `stability` | number | No | Voice stability setting from 0.0 to 1.0 \(e.g., 0.5 for balanced, 0.75 for more stable\). Higher values produce more consistent output | -| `similarity` | number | No | Similarity boost setting from 0.0 to 1.0 \(e.g., 0.75 for natural, 1.0 for maximum similarity\). Higher values make the voice more similar to the original | +| `similarityBoost` | number | No | Similarity boost setting from 0.0 to 1.0 \(e.g., 0.75 for natural, 1.0 for maximum similarity\). Higher values make the voice more similar to the original | | `apiKey` | string | Yes | Your ElevenLabs API key | #### Output diff --git a/apps/docs/content/docs/en/tools/google_slides.mdx b/apps/docs/content/docs/en/tools/google_slides.mdx index 30d6a17d404..ef62d931d49 100644 --- a/apps/docs/content/docs/en/tools/google_slides.mdx +++ b/apps/docs/content/docs/en/tools/google_slides.mdx @@ -30,7 +30,7 @@ In Sim, the Google Slides integration enables your agents to interact directly w ## Usage Instructions -Integrate Google Slides into the workflow. Can read, write, create presentations, replace text, add slides, add images, get thumbnails, get page details, delete objects, duplicate objects, reorder slides, create tables, create shapes, and insert text. +Build, edit, and export branded Google Slides presentations end-to-end. Copy a template, replace text and image tokens, embed Sheets charts, style text and shapes with brand fonts and colors, manage tables and layouts, group elements, run atomic batch updates, and export to PDF or PPTX. @@ -380,4 +380,1023 @@ Insert text into a shape or table cell in a Google Slides presentation. Use this | ↳ `presentationId` | string | The presentation ID | | ↳ `url` | string | URL to the presentation | +### `google_slides_update_text_style` + +Update the styling of text in a shape or table cell (bold, italic, font family, font size, foreground/background color, link, etc.). Only the fields you set are applied. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `presentationId` | string | Yes | Google Slides presentation ID | +| `objectId` | string | Yes | Object ID of the shape or table containing the text | +| `rowIndex` | number | No | When targeting a table cell, the zero-based row index | +| `columnIndex` | number | No | When targeting a table cell, the zero-based column index | +| `rangeType` | string | No | Range to style: ALL \(default\), FROM_START_INDEX, or FIXED_RANGE | +| `startIndex` | number | No | Start index for FROM_START_INDEX or FIXED_RANGE | +| `endIndex` | number | No | End index for FIXED_RANGE | +| `bold` | boolean | No | Whether the text is bold | +| `italic` | boolean | No | Whether the text is italic | +| `underline` | boolean | No | Whether the text is underlined | +| `strikethrough` | boolean | No | Whether the text has strikethrough | +| `smallCaps` | boolean | No | Whether the text is rendered in small caps | +| `fontFamily` | string | No | Font family name \(must be a font available to Google Slides\) | +| `fontSize` | number | No | Font size in points | +| `foregroundColor` | string | No | Text color as hex \(e.g. #1A73E8\) | +| `backgroundColor` | string | No | Text background color as hex \(e.g. #FFF8E1\) | +| `linkUrl` | string | No | Convert the range to a hyperlink with this URL | +| `baselineOffset` | string | No | Baseline offset: NONE, SUPERSCRIPT, or SUBSCRIPT | +| `styleJson` | string | No | Advanced: raw TextStyle JSON merged with the simple fields above \(overrides them on conflict\) | +| `fields` | string | No | Advanced: explicit FieldMask. If omitted, the mask is computed from the fields you provided \(or "*" when styleJson is used without explicit fields\). | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `updated` | boolean | Whether the text style was updated | +| `objectId` | string | The object whose text was styled | +| `fields` | string | FieldMask applied | +| `metadata` | object | Operation metadata | +| ↳ `presentationId` | string | The presentation ID | +| ↳ `url` | string | URL to the presentation | + +### `google_slides_update_paragraph_style` + +Update paragraph styling — alignment, line spacing, indents, space above/below — for text in a shape or table cell. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `presentationId` | string | Yes | Google Slides presentation ID | +| `objectId` | string | Yes | Object ID of the shape or table containing the text | +| `rowIndex` | number | No | When targeting a table cell, the zero-based row index | +| `columnIndex` | number | No | When targeting a table cell, the zero-based column index | +| `rangeType` | string | No | Range to style: ALL \(default\), FROM_START_INDEX, or FIXED_RANGE | +| `startIndex` | number | No | Start index for FROM_START_INDEX or FIXED_RANGE | +| `endIndex` | number | No | End index for FIXED_RANGE | +| `alignment` | string | No | Text alignment: START, CENTER, END, or JUSTIFIED | +| `lineSpacing` | number | No | Line spacing as a percentage \(100 = single, 200 = double\) | +| `indentStart` | number | No | Start-edge indent in points | +| `indentEnd` | number | No | End-edge indent in points | +| `indentFirstLine` | number | No | First-line indent in points | +| `spaceAbove` | number | No | Space above the paragraph in points | +| `spaceBelow` | number | No | Space below the paragraph in points | +| `direction` | string | No | Text direction: LEFT_TO_RIGHT or RIGHT_TO_LEFT | +| `spacingMode` | string | No | Spacing mode: NEVER_COLLAPSE or COLLAPSE_LISTS | +| `styleJson` | string | No | Advanced: raw ParagraphStyle JSON merged with the simple fields above | +| `fields` | string | No | Advanced: explicit FieldMask. If omitted, computed from provided fields. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `updated` | boolean | Whether the paragraph style was updated | +| `objectId` | string | The object whose paragraph was styled | +| `fields` | string | FieldMask applied | +| `metadata` | object | Operation metadata | +| ↳ `presentationId` | string | The presentation ID | +| ↳ `url` | string | URL to the presentation | + +### `google_slides_delete_text` + +Delete text from a shape or table cell. Use range type ALL to clear all text, or FIXED_RANGE / FROM_START_INDEX to delete a specific span. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `presentationId` | string | Yes | Google Slides presentation ID | +| `objectId` | string | Yes | Object ID of the shape or table containing the text | +| `rowIndex` | number | No | When targeting a table cell, the zero-based row index | +| `columnIndex` | number | No | When targeting a table cell, the zero-based column index | +| `rangeType` | string | No | Range to delete: ALL \(default\), FROM_START_INDEX, or FIXED_RANGE | +| `startIndex` | number | No | Start index for FROM_START_INDEX or FIXED_RANGE | +| `endIndex` | number | No | End index for FIXED_RANGE | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `deleted` | boolean | Whether the text was deleted | +| `objectId` | string | The object whose text was deleted | +| `metadata` | object | Operation metadata | +| ↳ `presentationId` | string | The presentation ID | +| ↳ `url` | string | URL to the presentation | + +### `google_slides_create_paragraph_bullets` + +Convert paragraphs in a shape or table cell into a bulleted or numbered list using a Google Slides bullet preset. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `presentationId` | string | Yes | Google Slides presentation ID | +| `objectId` | string | Yes | Object ID of the shape or table containing the text | +| `rowIndex` | number | No | When targeting a table cell, the zero-based row index | +| `columnIndex` | number | No | When targeting a table cell, the zero-based column index | +| `rangeType` | string | No | Range to apply bullets to: ALL \(default\), FROM_START_INDEX, or FIXED_RANGE | +| `startIndex` | number | No | Start index for FROM_START_INDEX or FIXED_RANGE | +| `endIndex` | number | No | End index for FIXED_RANGE | +| `bulletPreset` | string | No | Bullet preset \(e.g. BULLET_DISC_CIRCLE_SQUARE, BULLET_ARROW_DIAMOND_DISC, NUMBERED_DIGIT_ALPHA_ROMAN, NUMBERED_DIGIT_ALPHA_ROMAN_PARENS, NUMBERED_DIGIT_NESTED\). Defaults to BULLET_DISC_CIRCLE_SQUARE. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `created` | boolean | Whether bullets were created | +| `objectId` | string | The object where bullets were created | +| `metadata` | object | Operation metadata | +| ↳ `presentationId` | string | The presentation ID | +| ↳ `url` | string | URL to the presentation | + +### `google_slides_delete_paragraph_bullets` + +Remove bullets/numbering from paragraphs in a shape or table cell. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `presentationId` | string | Yes | Google Slides presentation ID | +| `objectId` | string | Yes | Object ID of the shape or table containing the text | +| `rowIndex` | number | No | When targeting a table cell, the zero-based row index | +| `columnIndex` | number | No | When targeting a table cell, the zero-based column index | +| `rangeType` | string | No | Range to clear bullets from: ALL \(default\), FROM_START_INDEX, or FIXED_RANGE | +| `startIndex` | number | No | Start index for FROM_START_INDEX or FIXED_RANGE | +| `endIndex` | number | No | End index for FIXED_RANGE | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `deleted` | boolean | Whether bullets were deleted | +| `objectId` | string | The object whose bullets were deleted | +| `metadata` | object | Operation metadata | +| ↳ `presentationId` | string | The presentation ID | +| ↳ `url` | string | URL to the presentation | + +### `google_slides_replace_all_shapes_with_image` + +Find every shape whose text matches the given token (e.g. \{\{cover-image\}\}) and replace it with an image, preserving the shape + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `presentationId` | string | Yes | Google Slides presentation ID | +| `imageUrl` | string | Yes | Publicly fetchable image URL \(PNG, JPEG, or GIF; max 50 MB and accessible to Google's servers\) | +| `findText` | string | Yes | Text content of shapes to replace \(e.g. \{\{cover-image\}\}\) | +| `matchCase` | boolean | No | Case-sensitive match \(default: true\) | +| `imageReplaceMethod` | string | No | How the image fits the shape: CENTER_INSIDE \(preserve aspect, fit inside\) or CENTER_CROP \(fill, crop overflow\). Default: CENTER_INSIDE. | +| `pageObjectIds` | string | No | Comma-separated slide IDs to limit replacement to specific slides | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `occurrencesChanged` | number | Number of shapes that were replaced with the image | +| `metadata` | object | Operation metadata | +| ↳ `presentationId` | string | The presentation ID | +| ↳ `url` | string | URL to the presentation | +| ↳ `imageUrl` | string | The image URL inserted | +| ↳ `findText` | string | The matched text token | + +### `google_slides_replace_image` + +Replace the source of an existing image with a new image URL, preserving the image + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `presentationId` | string | Yes | Google Slides presentation ID | +| `imageObjectId` | string | Yes | Object ID of the existing image to replace | +| `imageUrl` | string | Yes | New publicly fetchable image URL \(PNG, JPEG, or GIF, max 50 MB\) | +| `imageReplaceMethod` | string | No | CENTER_INSIDE \(preserve aspect\) or CENTER_CROP \(fill, crop overflow\). Default: CENTER_INSIDE. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `replaced` | boolean | Whether the image was replaced | +| `imageObjectId` | string | The image object that was replaced | +| `metadata` | object | Operation metadata | +| ↳ `presentationId` | string | The presentation ID | +| ↳ `url` | string | URL to the presentation | +| ↳ `imageUrl` | string | The new image URL | + +### `google_slides_update_image_properties` + +Update image properties — brightness, contrast, transparency, crop, outline, link — on an existing image. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `presentationId` | string | Yes | Google Slides presentation ID | +| `objectId` | string | Yes | Object ID of the image to update | +| `brightness` | number | No | Brightness adjustment between -1.0 and 1.0 | +| `contrast` | number | No | Contrast adjustment between -1.0 and 1.0 | +| `transparency` | number | No | Transparency between 0.0 \(opaque\) and 1.0 \(fully transparent\) | +| `linkUrl` | string | No | Make the image a hyperlink to this URL | +| `outlineColor` | string | No | Outline color as hex \(e.g. #1A73E8\) | +| `outlineWeight` | number | No | Outline weight in points | +| `outlineDashStyle` | string | No | Outline dash style: SOLID, DOT, DASH, DASH_DOT, LONG_DASH, LONG_DASH_DOT | +| `cropLeftOffset` | number | No | Crop offset from left edge \(0.0 to 1.0\) | +| `cropRightOffset` | number | No | Crop offset from right edge \(0.0 to 1.0\) | +| `cropTopOffset` | number | No | Crop offset from top edge \(0.0 to 1.0\) | +| `cropBottomOffset` | number | No | Crop offset from bottom edge \(0.0 to 1.0\) | +| `cropAngle` | number | No | Crop rotation angle in radians \(clockwise\) | +| `propertiesJson` | string | No | Advanced: raw ImageProperties JSON merged with the simple fields above | +| `fields` | string | No | Advanced: explicit FieldMask. If omitted, computed from provided fields. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `updated` | boolean | Whether the image properties were updated | +| `objectId` | string | The image object updated | +| `fields` | string | FieldMask applied | +| `metadata` | object | Operation metadata | +| ↳ `presentationId` | string | The presentation ID | +| ↳ `url` | string | URL to the presentation | + +### `google_slides_update_shape_properties` + +Update a shape + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `presentationId` | string | Yes | Google Slides presentation ID | +| `objectId` | string | Yes | Object ID of the shape to update | +| `fillColor` | string | No | Solid background fill color as hex \(e.g. #FF6F61\) | +| `fillAlpha` | number | No | Fill opacity between 0.0 \(transparent\) and 1.0 \(opaque\) | +| `fillUnset` | boolean | No | When true, removes any fill so the shape inherits its layout/master fill | +| `outlineColor` | string | No | Outline color as hex | +| `outlineWeight` | number | No | Outline weight in points | +| `outlineDashStyle` | string | No | Outline dash style: SOLID, DOT, DASH, DASH_DOT, LONG_DASH, LONG_DASH_DOT | +| `outlineUnset` | boolean | No | When true, removes any outline so the shape inherits its layout/master outline | +| `linkUrl` | string | No | Make the shape a hyperlink to this URL | +| `contentAlignment` | string | No | Vertical alignment of shape contents: TOP, MIDDLE, or BOTTOM | +| `autofitType` | string | No | Autofit behavior: NONE, TEXT_AUTOFIT, or SHAPE_AUTOFIT | +| `propertiesJson` | string | No | Advanced: raw ShapeProperties JSON merged with the simple fields above | +| `fields` | string | No | Advanced: explicit FieldMask. If omitted, computed from provided fields. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `updated` | boolean | Whether the shape properties were updated | +| `objectId` | string | The shape object updated | +| `fields` | string | FieldMask applied | +| `metadata` | object | Operation metadata | +| ↳ `presentationId` | string | The presentation ID | +| ↳ `url` | string | URL to the presentation | + +### `google_slides_update_page_properties` + +Update slide/page background — solid color or stretched picture — and other page properties. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `presentationId` | string | Yes | Google Slides presentation ID | +| `objectId` | string | Yes | Object ID of the slide/page to update | +| `backgroundColor` | string | No | Solid background color as hex \(e.g. #0B1F3A\) | +| `backgroundAlpha` | number | No | Background fill opacity between 0.0 and 1.0 | +| `backgroundImageUrl` | string | No | Publicly fetchable image URL to use as a stretched picture background | +| `backgroundUnset` | boolean | No | When true, removes the background so the slide inherits its layout background | +| `propertiesJson` | string | No | Advanced: raw PageProperties JSON merged with the simple fields above | +| `fields` | string | No | Advanced: explicit FieldMask. If omitted, computed from provided fields. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `updated` | boolean | Whether the page properties were updated | +| `objectId` | string | The page object updated | +| `fields` | string | FieldMask applied | +| `metadata` | object | Operation metadata | +| ↳ `presentationId` | string | The presentation ID | +| ↳ `url` | string | URL to the presentation | + +### `google_slides_update_slide_properties` + +Update slide-specific properties such as whether the slide is skipped during presentation. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `presentationId` | string | Yes | Google Slides presentation ID | +| `objectId` | string | Yes | Object ID of the slide to update | +| `isSkipped` | boolean | No | Whether the slide is skipped in presentation mode | +| `propertiesJson` | string | No | Advanced: raw SlideProperties JSON merged with the simple fields above | +| `fields` | string | No | Advanced: explicit FieldMask. If omitted, computed from provided fields. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `updated` | boolean | Whether the slide properties were updated | +| `objectId` | string | The slide object updated | +| `fields` | string | FieldMask applied | +| `metadata` | object | Operation metadata | +| ↳ `presentationId` | string | The presentation ID | +| ↳ `url` | string | URL to the presentation | + +### `google_slides_update_page_element_alt_text` + +Set the accessibility title and/or description (alt text) of a page element such as an image, shape, or group. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `presentationId` | string | Yes | Google Slides presentation ID | +| `objectId` | string | Yes | Object ID of the page element | +| `title` | string | No | Accessibility title for the element | +| `description` | string | No | Accessibility description \(alt text\) for the element | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `updated` | boolean | Whether alt text was updated | +| `objectId` | string | The element updated | +| `metadata` | object | Operation metadata | +| ↳ `presentationId` | string | The presentation ID | +| ↳ `url` | string | URL to the presentation | + +### `google_slides_update_page_element_transform` + +Move, resize, scale, or shear a page element. Translate is specified in points; applyMode controls whether the transform is absolute (default) or relative (multiplied with the current transform). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `presentationId` | string | Yes | Google Slides presentation ID | +| `objectId` | string | Yes | Object ID of the page element to transform | +| `scaleX` | number | No | Horizontal scale factor \(default 1\) | +| `scaleY` | number | No | Vertical scale factor \(default 1\) | +| `shearX` | number | No | Horizontal shear factor \(default 0\) | +| `shearY` | number | No | Vertical shear factor \(default 0\) | +| `translateX` | number | No | X position in points \(absolute\) or delta \(relative\) | +| `translateY` | number | No | Y position in points \(absolute\) or delta \(relative\) | +| `applyMode` | string | No | ABSOLUTE replaces the current transform; RELATIVE multiplies with it. Default ABSOLUTE. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `updated` | boolean | Whether the transform was updated | +| `objectId` | string | The element transformed | +| `metadata` | object | Operation metadata | +| ↳ `presentationId` | string | The presentation ID | +| ↳ `url` | string | URL to the presentation | + +### `google_slides_update_page_elements_z_order` + +Bring elements to front, send to back, or step them one layer forward/backward. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `presentationId` | string | Yes | Google Slides presentation ID | +| `objectIds` | string | Yes | Comma-separated object IDs of the elements to reorder | +| `operation` | string | Yes | BRING_TO_FRONT, BRING_FORWARD, SEND_BACKWARD, or SEND_TO_BACK | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `reordered` | boolean | Whether the z-order was changed | +| `objectIds` | array | Elements reordered | +| `operation` | string | Operation applied | +| `metadata` | object | Operation metadata | +| ↳ `presentationId` | string | The presentation ID | +| ↳ `url` | string | URL to the presentation | + +### `google_slides_group_objects` + +Group two or more page elements on the same slide into a single object group. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `presentationId` | string | Yes | Google Slides presentation ID | +| `childrenObjectIds` | string | Yes | Comma-separated object IDs of the elements to group \(must be on the same slide\) | +| `groupObjectId` | string | No | Optional object ID to assign to the new group | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `grouped` | boolean | Whether the objects were grouped | +| `groupObjectId` | string | Object ID of the new group | +| `childrenObjectIds` | array | IDs of the grouped children | +| `metadata` | object | Operation metadata | +| ↳ `presentationId` | string | The presentation ID | +| ↳ `url` | string | URL to the presentation | + +### `google_slides_ungroup_objects` + +Ungroup one or more object groups, releasing their children back to the slide. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `presentationId` | string | Yes | Google Slides presentation ID | +| `objectIds` | string | Yes | Comma-separated object IDs of the groups to ungroup | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `ungrouped` | boolean | Whether the objects were ungrouped | +| `objectIds` | array | Group IDs that were ungrouped | +| `metadata` | object | Operation metadata | +| ↳ `presentationId` | string | The presentation ID | +| ↳ `url` | string | URL to the presentation | + +### `google_slides_create_line` + +Create a line or connector on a slide. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `presentationId` | string | Yes | Google Slides presentation ID | +| `pageObjectId` | string | Yes | Object ID of the slide to add the line to | +| `lineCategory` | string | No | STRAIGHT \(default\), BENT, or CURVED | +| `width` | number | No | Line width in points \(default 200\) | +| `height` | number | No | Line height in points \(default 0 — horizontal line\) | +| `positionX` | number | No | X position in points \(default 100\) | +| `positionY` | number | No | Y position in points \(default 100\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `lineId` | string | Object ID of the new line | +| `lineCategory` | string | Line category created | +| `metadata` | object | Operation metadata | +| ↳ `presentationId` | string | The presentation ID | +| ↳ `pageObjectId` | string | The slide ID | +| ↳ `url` | string | URL to the presentation | + +### `google_slides_update_line_properties` + +Update line appearance — color, weight, dash style, arrows, link. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `presentationId` | string | Yes | Google Slides presentation ID | +| `objectId` | string | Yes | Object ID of the line | +| `lineColor` | string | No | Line color as hex | +| `lineWeight` | number | No | Line weight in points | +| `dashStyle` | string | No | Dash style: SOLID, DOT, DASH, DASH_DOT, LONG_DASH, LONG_DASH_DOT | +| `startArrow` | string | No | Start arrow style: NONE, STEALTH_ARROW, FILL_ARROW, FILL_CIRCLE, FILL_SQUARE, FILL_DIAMOND, OPEN_ARROW, OPEN_CIRCLE, OPEN_SQUARE, OPEN_DIAMOND | +| `endArrow` | string | No | End arrow style \(same values as startArrow\) | +| `linkUrl` | string | No | Hyperlink URL | +| `propertiesJson` | string | No | Advanced: raw LineProperties JSON merged with the simple fields above | +| `fields` | string | No | Advanced: explicit FieldMask. If omitted, computed from provided fields. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `updated` | boolean | Whether the line properties were updated | +| `objectId` | string | The line object updated | +| `fields` | string | FieldMask applied | +| `metadata` | object | Operation metadata | +| ↳ `presentationId` | string | The presentation ID | +| ↳ `url` | string | URL to the presentation | + +### `google_slides_update_line_category` + +Change a connector line + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `presentationId` | string | Yes | Google Slides presentation ID | +| `objectId` | string | Yes | Object ID of the connector line | +| `lineCategory` | string | Yes | New line category: STRAIGHT, BENT, or CURVED | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `updated` | boolean | Whether the line category was updated | +| `objectId` | string | The line object updated | +| `lineCategory` | string | New line category | +| `metadata` | object | Operation metadata | +| ↳ `presentationId` | string | The presentation ID | +| ↳ `url` | string | URL to the presentation | + +### `google_slides_reroute_line` + +Reroute a connector line so it efficiently connects its endpoint shapes — useful after moving the shapes the line connects. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `presentationId` | string | Yes | Google Slides presentation ID | +| `objectId` | string | Yes | Object ID of the connector line to reroute | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `rerouted` | boolean | Whether the line was rerouted | +| `objectId` | string | The line object rerouted | +| `metadata` | object | Operation metadata | +| ↳ `presentationId` | string | The presentation ID | +| ↳ `url` | string | URL to the presentation | + +### `google_slides_insert_table_rows` + +Insert one or more rows into a table, above or below a reference cell. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `presentationId` | string | Yes | Google Slides presentation ID | +| `tableObjectId` | string | Yes | Object ID of the table | +| `rowIndex` | number | Yes | Zero-based row index of the reference cell | +| `columnIndex` | number | Yes | Zero-based column index of the reference cell | +| `number` | number | Yes | Number of rows to insert \(minimum 1\) | +| `insertBelow` | boolean | No | Insert below the reference row instead of above \(default false\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `inserted` | boolean | Whether rows were inserted | +| `tableObjectId` | string | The table updated | +| `number` | number | Number of rows inserted | +| `metadata` | object | Operation metadata | +| ↳ `presentationId` | string | The presentation ID | +| ↳ `url` | string | URL to the presentation | + +### `google_slides_insert_table_columns` + +Insert one or more columns into a table, left or right of a reference cell. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `presentationId` | string | Yes | Google Slides presentation ID | +| `tableObjectId` | string | Yes | Object ID of the table | +| `rowIndex` | number | Yes | Zero-based row index of the reference cell | +| `columnIndex` | number | Yes | Zero-based column index of the reference cell | +| `number` | number | Yes | Number of columns to insert \(minimum 1\) | +| `insertRight` | boolean | No | Insert to the right of the reference column instead of left \(default false\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `inserted` | boolean | Whether columns were inserted | +| `tableObjectId` | string | The table updated | +| `number` | number | Number of columns inserted | +| `metadata` | object | Operation metadata | +| ↳ `presentationId` | string | The presentation ID | +| ↳ `url` | string | URL to the presentation | + +### `google_slides_delete_table_row` + +Delete the row containing the reference cell from a table. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `presentationId` | string | Yes | Google Slides presentation ID | +| `tableObjectId` | string | Yes | Object ID of the table | +| `rowIndex` | number | Yes | Zero-based row index identifying the row to delete | +| `columnIndex` | number | Yes | Zero-based column index of any cell in the row | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `deleted` | boolean | Whether the row was deleted | +| `tableObjectId` | string | The table updated | +| `metadata` | object | Operation metadata | +| ↳ `presentationId` | string | The presentation ID | +| ↳ `url` | string | URL to the presentation | + +### `google_slides_delete_table_column` + +Delete the column containing the reference cell from a table. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `presentationId` | string | Yes | Google Slides presentation ID | +| `tableObjectId` | string | Yes | Object ID of the table | +| `rowIndex` | number | Yes | Zero-based row index of any cell in the column | +| `columnIndex` | number | Yes | Zero-based column index identifying the column to delete | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `deleted` | boolean | Whether the column was deleted | +| `tableObjectId` | string | The table updated | +| `metadata` | object | Operation metadata | +| ↳ `presentationId` | string | The presentation ID | +| ↳ `url` | string | URL to the presentation | + +### `google_slides_merge_table_cells` + +Merge a rectangular range of table cells into a single cell. The range starts at (rowIndex, columnIndex) and covers rowSpan × columnSpan cells. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `presentationId` | string | Yes | Google Slides presentation ID | +| `objectId` | string | Yes | Object ID of the table | +| `rowIndex` | number | Yes | Zero-based row index of the top-left cell | +| `columnIndex` | number | Yes | Zero-based column index of the top-left cell | +| `rowSpan` | number | Yes | Number of rows to merge \(minimum 1\) | +| `columnSpan` | number | Yes | Number of columns to merge \(minimum 1\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `merged` | boolean | Whether the cells were merged | +| `objectId` | string | The table updated | +| `metadata` | object | Operation metadata | +| ↳ `presentationId` | string | The presentation ID | +| ↳ `url` | string | URL to the presentation | + +### `google_slides_unmerge_table_cells` + +Unmerge any merged cells within the given table range. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `presentationId` | string | Yes | Google Slides presentation ID | +| `objectId` | string | Yes | Object ID of the table | +| `rowIndex` | number | Yes | Zero-based row index of the top-left cell of the range | +| `columnIndex` | number | Yes | Zero-based column index of the top-left cell of the range | +| `rowSpan` | number | Yes | Number of rows in the range \(minimum 1\) | +| `columnSpan` | number | Yes | Number of columns in the range \(minimum 1\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `unmerged` | boolean | Whether the cells were unmerged | +| `objectId` | string | The table updated | +| `metadata` | object | Operation metadata | +| ↳ `presentationId` | string | The presentation ID | +| ↳ `url` | string | URL to the presentation | + +### `google_slides_update_table_cell_properties` + +Update background fill and content alignment for a range of table cells. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `presentationId` | string | Yes | Google Slides presentation ID | +| `objectId` | string | Yes | Object ID of the table | +| `rowIndex` | number | Yes | Zero-based row index of the top-left cell of the range | +| `columnIndex` | number | Yes | Zero-based column index of the top-left cell of the range | +| `rowSpan` | number | Yes | Number of rows in the range \(minimum 1\) | +| `columnSpan` | number | Yes | Number of columns in the range \(minimum 1\) | +| `backgroundColor` | string | No | Cell background color as hex \(e.g. #F1F3F4\) | +| `backgroundAlpha` | number | No | Background fill opacity between 0.0 and 1.0 | +| `contentAlignment` | string | No | Vertical alignment of cell content: TOP, MIDDLE, or BOTTOM | +| `propertiesJson` | string | No | Advanced: raw TableCellProperties JSON merged with the simple fields above | +| `fields` | string | No | Advanced: explicit FieldMask. If omitted, computed from provided fields. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `updated` | boolean | Whether the cell properties were updated | +| `objectId` | string | The table updated | +| `fields` | string | FieldMask applied | +| `metadata` | object | Operation metadata | +| ↳ `presentationId` | string | The presentation ID | +| ↳ `url` | string | URL to the presentation | + +### `google_slides_update_table_border_properties` + +Update border color, weight, and dash style for a position (e.g. ALL, INNER, OUTER) in a table range. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `presentationId` | string | Yes | Google Slides presentation ID | +| `objectId` | string | Yes | Object ID of the table | +| `rowIndex` | number | Yes | Zero-based row index of the top-left cell of the range | +| `columnIndex` | number | Yes | Zero-based column index of the top-left cell of the range | +| `rowSpan` | number | Yes | Number of rows in the range \(minimum 1\) | +| `columnSpan` | number | Yes | Number of columns in the range \(minimum 1\) | +| `borderPosition` | string | No | Which borders to update: ALL \(default\), BOTTOM, INNER, INNER_HORIZONTAL, INNER_VERTICAL, LEFT, OUTER, RIGHT, TOP | +| `borderColor` | string | No | Border color as hex | +| `borderWeight` | number | No | Border weight in points | +| `dashStyle` | string | No | Dash style: SOLID, DOT, DASH, DASH_DOT, LONG_DASH, LONG_DASH_DOT | +| `propertiesJson` | string | No | Advanced: raw TableBorderProperties JSON merged with the simple fields above | +| `fields` | string | No | Advanced: explicit FieldMask. If omitted, computed from provided fields. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `updated` | boolean | Whether the border properties were updated | +| `objectId` | string | The table updated | +| `fields` | string | FieldMask applied | +| `metadata` | object | Operation metadata | +| ↳ `presentationId` | string | The presentation ID | +| ↳ `url` | string | URL to the presentation | + +### `google_slides_update_table_column_properties` + +Update column widths and other column-level table properties. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `presentationId` | string | Yes | Google Slides presentation ID | +| `objectId` | string | Yes | Object ID of the table | +| `columnIndices` | string | Yes | Comma-separated zero-based column indices to update \(e.g. "0,2,3"\) | +| `columnWidth` | number | No | Column width in points | +| `propertiesJson` | string | No | Advanced: raw TableColumnProperties JSON merged with the simple fields above | +| `fields` | string | No | Advanced: explicit FieldMask. If omitted, computed from provided fields. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `updated` | boolean | Whether the column properties were updated | +| `objectId` | string | The table updated | +| `fields` | string | FieldMask applied | +| `metadata` | object | Operation metadata | +| ↳ `presentationId` | string | The presentation ID | +| ↳ `url` | string | URL to the presentation | + +### `google_slides_update_table_row_properties` + +Update minimum row heights and other row-level table properties. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `presentationId` | string | Yes | Google Slides presentation ID | +| `objectId` | string | Yes | Object ID of the table | +| `rowIndices` | string | Yes | Comma-separated zero-based row indices to update \(e.g. "0,2,3"\) | +| `minRowHeight` | number | No | Minimum row height in points | +| `propertiesJson` | string | No | Advanced: raw TableRowProperties JSON merged with the simple fields above | +| `fields` | string | No | Advanced: explicit FieldMask. If omitted, computed from provided fields. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `updated` | boolean | Whether the row properties were updated | +| `objectId` | string | The table updated | +| `fields` | string | FieldMask applied | +| `metadata` | object | Operation metadata | +| ↳ `presentationId` | string | The presentation ID | +| ↳ `url` | string | URL to the presentation | + +### `google_slides_create_sheets_chart` + +Embed a chart from a Google Sheets spreadsheet onto a slide. LINKED charts can be refreshed; NOT_LINKED_IMAGE inserts a static image of the chart. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `presentationId` | string | Yes | Google Slides presentation ID | +| `pageObjectId` | string | Yes | Object ID of the slide to add the chart to | +| `spreadsheetId` | string | Yes | Google Sheets spreadsheet ID containing the chart | +| `chartId` | number | Yes | Numeric chart ID within the spreadsheet | +| `linkingMode` | string | No | LINKED \(default\) or NOT_LINKED_IMAGE | +| `width` | number | No | Width in points \(default 400\) | +| `height` | number | No | Height in points \(default 300\) | +| `positionX` | number | No | X position in points \(default 100\) | +| `positionY` | number | No | Y position in points \(default 100\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `chartObjectId` | string | Object ID of the inserted chart | +| `metadata` | object | Operation metadata | +| ↳ `presentationId` | string | The presentation ID | +| ↳ `pageObjectId` | string | The slide ID | +| ↳ `url` | string | URL to the presentation | + +### `google_slides_refresh_sheets_chart` + +Refresh an embedded linked Sheets chart so it reflects the latest spreadsheet data. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `presentationId` | string | Yes | Google Slides presentation ID | +| `objectId` | string | Yes | Object ID of the embedded chart to refresh | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `refreshed` | boolean | Whether the chart was refreshed | +| `objectId` | string | The chart object refreshed | +| `metadata` | object | Operation metadata | +| ↳ `presentationId` | string | The presentation ID | +| ↳ `url` | string | URL to the presentation | + +### `google_slides_replace_all_shapes_with_sheets_chart` + +Find every shape matching a text token (e.g. \{\{revenue-chart\}\}) and replace each with the same embedded Sheets chart, preserving the shape + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `presentationId` | string | Yes | Google Slides presentation ID | +| `spreadsheetId` | string | Yes | Google Sheets spreadsheet ID containing the chart | +| `chartId` | number | Yes | Numeric chart ID within the spreadsheet | +| `findText` | string | Yes | Text content of shapes to replace \(e.g. \{\{revenue-chart\}\}\) | +| `matchCase` | boolean | No | Case-sensitive match \(default true\) | +| `linkingMode` | string | No | LINKED \(default\) or NOT_LINKED_IMAGE | +| `pageObjectIds` | string | No | Comma-separated slide IDs to limit replacement to specific slides | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `occurrencesChanged` | number | Number of shapes replaced with the chart | +| `metadata` | object | Operation metadata | +| ↳ `presentationId` | string | The presentation ID | +| ↳ `url` | string | URL to the presentation | +| ↳ `findText` | string | The matched text token | +| ↳ `spreadsheetId` | string | Source spreadsheet ID | +| ↳ `chartId` | number | Source chart ID | + +### `google_slides_create_video` + +Embed a YouTube or Google Drive video on a slide. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `presentationId` | string | Yes | Google Slides presentation ID | +| `pageObjectId` | string | Yes | Object ID of the slide to add the video to | +| `source` | string | Yes | YOUTUBE or DRIVE | +| `videoId` | string | Yes | YouTube video ID or Drive file ID | +| `width` | number | No | Width in points \(default 400\) | +| `height` | number | No | Height in points \(default 225\) | +| `positionX` | number | No | X position in points \(default 100\) | +| `positionY` | number | No | Y position in points \(default 100\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `videoObjectId` | string | Object ID of the inserted video | +| `metadata` | object | Operation metadata | +| ↳ `presentationId` | string | The presentation ID | +| ↳ `pageObjectId` | string | The slide ID | +| ↳ `url` | string | URL to the presentation | + +### `google_slides_update_video_properties` + +Update video playback options (autoPlay, mute, start/end) and outline. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `presentationId` | string | Yes | Google Slides presentation ID | +| `objectId` | string | Yes | Object ID of the video | +| `autoPlay` | boolean | No | Play the video automatically when the slide is shown | +| `mute` | boolean | No | Mute the video | +| `start` | number | No | Playback start time in seconds | +| `end` | number | No | Playback end time in seconds | +| `outlineColor` | string | No | Outline color as hex | +| `outlineWeight` | number | No | Outline weight in points | +| `outlineDashStyle` | string | No | Outline dash style | +| `propertiesJson` | string | No | Advanced: raw VideoProperties JSON merged with the simple fields above | +| `fields` | string | No | Advanced: explicit FieldMask. If omitted, computed from provided fields. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `updated` | boolean | Whether the video properties were updated | +| `objectId` | string | The video object updated | +| `fields` | string | FieldMask applied | +| `metadata` | object | Operation metadata | +| ↳ `presentationId` | string | The presentation ID | +| ↳ `url` | string | URL to the presentation | + +### `google_slides_batch_update` + +Run a raw Slides API batchUpdate with a list of Request objects. Use this when the higher-level tools do not cover an operation, or to bundle multiple operations into a single atomic batch (all-or-nothing). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `presentationId` | string | Yes | Google Slides presentation ID | +| `requests` | string | Yes | JSON array of Slides API Request objects. Example: \[\{"replaceAllText":\{"containsText":\{"text":"\{\{title\}\}"\},"replaceText":"Q3 Review"\}\}, \{"updatePageProperties":\{"objectId":"slide_1","pageProperties":\{"pageBackgroundFill":\{"solidFill":\{"color":\{"rgbColor":\{"red":0.043,"green":0.122,"blue":0.231\}\}\}\}\},"fields":"pageBackgroundFill"\}\}\] | +| `writeControl` | string | No | Optional JSON WriteControl object for optimistic concurrency, e.g. \{"requiredRevisionId":"..."\} | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `replies` | array | Array of reply objects, one per request \(parallel-indexed\) | +| `writeControl` | json | WriteControl returned by the server \(revision tracking\) | +| `metadata` | object | Operation metadata | +| ↳ `presentationId` | string | The presentation ID | +| ↳ `url` | string | URL to the presentation | +| ↳ `requestCount` | number | Number of replies returned | + +### `google_slides_copy_presentation` + +Copy a template presentation in Drive to a new file. Use this before merging data so the original template is never modified. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `sourcePresentationId` | string | Yes | Drive file ID of the source/template presentation | +| `title` | string | No | Title for the copy. Defaults to "Copy of <source title>". | +| `folderId` | string | No | Drive folder ID where the copy should be placed | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `presentationId` | string | ID of the new copied presentation | +| `title` | string | Title of the new presentation | +| `metadata` | object | Operation metadata | +| ↳ `sourcePresentationId` | string | Source/template presentation ID | +| ↳ `presentationId` | string | New presentation ID | +| ↳ `title` | string | New presentation title | +| ↳ `mimeType` | string | MIME type of the presentation | +| ↳ `url` | string | URL to the new presentation | + +### `google_slides_export_presentation` + +Export a presentation to PDF, PPTX, ODP, TXT, PNG, JPEG, or SVG via the Drive export endpoint. Stores the exported file as an execution file when execution context is available. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `presentationId` | string | Yes | Google Slides presentation ID | +| `exportFormat` | string | No | Format: PDF \(default\), PPTX, ODP, TXT, PNG, JPEG, or SVG | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `file` | file | Stored exported presentation file | +| `contentBase64` | string | Deprecated legacy inline content. New exports return file. | +| `mimeType` | string | MIME type of the exported content | +| `sizeBytes` | number | Size of the exported content in bytes | +| `metadata` | object | Operation metadata | +| ↳ `presentationId` | string | The presentation ID | +| ↳ `url` | string | URL to the presentation | +| ↳ `exportFormat` | string | Export format used | + diff --git a/apps/docs/content/docs/en/tools/hubspot.mdx b/apps/docs/content/docs/en/tools/hubspot.mdx index 882f2766f10..1952ec4f457 100644 --- a/apps/docs/content/docs/en/tools/hubspot.mdx +++ b/apps/docs/content/docs/en/tools/hubspot.mdx @@ -26,7 +26,7 @@ In Sim, the HubSpot integration enables your agents to interact with your CRM da ## Usage Instructions -Integrate HubSpot into your workflow. Manage contacts, companies, deals, tickets, and other CRM objects with powerful automation capabilities. Can be used in trigger mode to start workflows when contacts are created, deleted, or updated. +Integrate HubSpot into your workflow. Manage contacts, companies, deals, tickets, and other CRM objects with powerful automation capabilities. Can be used in trigger mode to start workflows when records are created, updated, a specific property changes, or a contact joins a list. diff --git a/apps/docs/content/docs/en/tools/image_generator.mdx b/apps/docs/content/docs/en/tools/image_generator.mdx index f44eba6f1f6..360d0a4ca3f 100644 --- a/apps/docs/content/docs/en/tools/image_generator.mdx +++ b/apps/docs/content/docs/en/tools/image_generator.mdx @@ -58,6 +58,9 @@ Generate images with OpenAI GPT Image, Google Nano Banana, or Fal.ai image model | `enableSafetyChecker` | boolean | No | Enable the Fal.ai safety checker when supported | | `enableWebSearch` | boolean | No | Enable web search grounding when supported by the selected Fal.ai model | | `thinkingLevel` | string | No | Fal.ai thinking level when supported: minimal or high | +| `pricing` | custom | No | No description | +| `metadata` | string | No | No description | +| `rateLimit` | string | No | No description | #### Output diff --git a/apps/docs/content/docs/en/tools/instantly.mdx b/apps/docs/content/docs/en/tools/instantly.mdx index 811fefde4ca..feca45b362c 100644 --- a/apps/docs/content/docs/en/tools/instantly.mdx +++ b/apps/docs/content/docs/en/tools/instantly.mdx @@ -36,11 +36,15 @@ Retrieves Instantly V2 leads with search, campaign, list, and pagination filters | `items` | string | No | No description | | `excluded_ids` | array | No | Lead IDs to exclude | | `items` | string | No | No description | +| `organization_user_ids` | array | No | Organization user IDs to filter leads | +| `items` | string | No | No description | +| `smart_view_id` | string | No | Smart view ID to filter leads | | `contacts` | array | No | Lead email addresses to include | | `items` | string | No | No description | | `limit` | number | No | Number of leads to return, from 1 to 100 | | `starting_after` | string | No | Forward pagination cursor from next_starting_after | | `distinct_contacts` | boolean | No | Whether to return distinct contacts | +| `is_website_visitor` | boolean | No | Whether the lead is a website visitor | | `enrichment_status` | number | No | Enrichment status filter | | `esg_code` | string | No | Email security gateway code filter | @@ -48,14 +52,14 @@ Retrieves Instantly V2 leads with search, campaign, list, and pagination filters | Parameter | Type | Description | | --------- | ---- | ----------- | -| `leads` | array | List of leads | -| `lead` | json | Lead details | -| `campaigns` | array | List of campaigns | -| `campaign` | json | Campaign details | -| `emails` | array | List of emails | -| `email` | json | Email details | -| `lead_lists` | array | List of lead lists | -| `lead_list` | json | Lead list details | +| `leads` | array | List of leads \(id, email, first_name, last_name, campaign, status\) | +| `lead` | json | Lead details \(id, email, first_name, last_name, company_name, job_title, campaign, status, payload\) | +| `campaigns` | array | List of campaigns \(id, name, status, daily_limit\) | +| `campaign` | json | Campaign details \(id, name, status, daily_limit, daily_max_leads, open_tracking\) | +| `emails` | array | List of emails \(id, subject, from_address_email, lead, thread_id\) | +| `email` | json | Email details \(id, subject, from_address_email, to_address_email_list, thread_id, content_preview\) | +| `lead_lists` | array | List of lead lists \(id, name, has_enrichment_task, timestamp_created\) | +| `lead_list` | json | Lead list details \(id, organization_id, has_enrichment_task, owned_by, name, timestamp_created\) | | `count` | number | Returned or affected record count | | `next_starting_after` | string | Cursor for the next page | | `id` | string | Record ID | @@ -82,14 +86,14 @@ Retrieves an Instantly V2 lead by ID. | Parameter | Type | Description | | --------- | ---- | ----------- | -| `leads` | array | List of leads | -| `lead` | json | Lead details | -| `campaigns` | array | List of campaigns | -| `campaign` | json | Campaign details | -| `emails` | array | List of emails | -| `email` | json | Email details | -| `lead_lists` | array | List of lead lists | -| `lead_list` | json | Lead list details | +| `leads` | array | List of leads \(id, email, first_name, last_name, campaign, status\) | +| `lead` | json | Lead details \(id, email, first_name, last_name, company_name, job_title, campaign, status, payload\) | +| `campaigns` | array | List of campaigns \(id, name, status, daily_limit\) | +| `campaign` | json | Campaign details \(id, name, status, daily_limit, daily_max_leads, open_tracking\) | +| `emails` | array | List of emails \(id, subject, from_address_email, lead, thread_id\) | +| `email` | json | Email details \(id, subject, from_address_email, to_address_email_list, thread_id, content_preview\) | +| `lead_lists` | array | List of lead lists \(id, name, has_enrichment_task, timestamp_created\) | +| `lead_list` | json | Lead list details \(id, organization_id, has_enrichment_task, owned_by, name, timestamp_created\) | | `count` | number | Returned or affected record count | | `next_starting_after` | string | Cursor for the next page | | `id` | string | Record ID | @@ -127,6 +131,7 @@ Creates an Instantly V2 lead in a campaign or lead list. | `skip_if_in_campaign` | boolean | No | Skip if the lead already exists in the campaign | | `skip_if_in_list` | boolean | No | Skip if the lead already exists in the list | | `blocklist_id` | string | No | Blocklist ID to check for the lead | +| `verify_leads_for_lead_finder` | boolean | No | Whether to verify leads imported from Lead Finder | | `verify_leads_on_import` | boolean | No | Whether to verify leads on import | | `custom_variables` | json | No | Custom variable object with string, number, boolean, or null values | @@ -134,14 +139,14 @@ Creates an Instantly V2 lead in a campaign or lead list. | Parameter | Type | Description | | --------- | ---- | ----------- | -| `leads` | array | List of leads | -| `lead` | json | Lead details | -| `campaigns` | array | List of campaigns | -| `campaign` | json | Campaign details | -| `emails` | array | List of emails | -| `email` | json | Email details | -| `lead_lists` | array | List of lead lists | -| `lead_list` | json | Lead list details | +| `leads` | array | List of leads \(id, email, first_name, last_name, campaign, status\) | +| `lead` | json | Lead details \(id, email, first_name, last_name, company_name, job_title, campaign, status, payload\) | +| `campaigns` | array | List of campaigns \(id, name, status, daily_limit\) | +| `campaign` | json | Campaign details \(id, name, status, daily_limit, daily_max_leads, open_tracking\) | +| `emails` | array | List of emails \(id, subject, from_address_email, lead, thread_id\) | +| `email` | json | Email details \(id, subject, from_address_email, to_address_email_list, thread_id, content_preview\) | +| `lead_lists` | array | List of lead lists \(id, name, has_enrichment_task, timestamp_created\) | +| `lead_list` | json | Lead list details \(id, organization_id, has_enrichment_task, owned_by, name, timestamp_created\) | | `count` | number | Returned or affected record count | | `next_starting_after` | string | Cursor for the next page | | `id` | string | Record ID | @@ -184,7 +189,7 @@ Submits an Instantly V2 background job to update a lead interest status. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `lead_email` | string | Yes | Lead email address | -| `interest_value` | number | Yes | Interest status value. Use null in JSON/tool input to reset to Lead. | +| `interest_value` | number | No | Interest status value. Leave empty in the block or pass null to reset to Lead. | | `campaign_id` | string | No | Campaign ID for the lead | | `list_id` | string | No | Lead list ID for the lead | | `ai_interest_value` | number | No | AI interest value to set for the lead | @@ -215,14 +220,14 @@ Retrieves Instantly V2 campaigns with search, status, tag, and pagination filter | Parameter | Type | Description | | --------- | ---- | ----------- | -| `leads` | array | List of leads | -| `lead` | json | Lead details | -| `campaigns` | array | List of campaigns | -| `campaign` | json | Campaign details | -| `emails` | array | List of emails | -| `email` | json | Email details | -| `lead_lists` | array | List of lead lists | -| `lead_list` | json | Lead list details | +| `leads` | array | List of leads \(id, email, first_name, last_name, campaign, status\) | +| `lead` | json | Lead details \(id, email, first_name, last_name, company_name, job_title, campaign, status, payload\) | +| `campaigns` | array | List of campaigns \(id, name, status, daily_limit\) | +| `campaign` | json | Campaign details \(id, name, status, daily_limit, daily_max_leads, open_tracking\) | +| `emails` | array | List of emails \(id, subject, from_address_email, lead, thread_id\) | +| `email` | json | Email details \(id, subject, from_address_email, to_address_email_list, thread_id, content_preview\) | +| `lead_lists` | array | List of lead lists \(id, name, has_enrichment_task, timestamp_created\) | +| `lead_list` | json | Lead list details \(id, organization_id, has_enrichment_task, owned_by, name, timestamp_created\) | | `count` | number | Returned or affected record count | | `next_starting_after` | string | Cursor for the next page | | `id` | string | Record ID | @@ -262,14 +267,14 @@ Creates an Instantly V2 campaign using the documented campaign schedule schema. | Parameter | Type | Description | | --------- | ---- | ----------- | -| `leads` | array | List of leads | -| `lead` | json | Lead details | -| `campaigns` | array | List of campaigns | -| `campaign` | json | Campaign details | -| `emails` | array | List of emails | -| `email` | json | Email details | -| `lead_lists` | array | List of lead lists | -| `lead_list` | json | Lead list details | +| `leads` | array | List of leads \(id, email, first_name, last_name, campaign, status\) | +| `lead` | json | Lead details \(id, email, first_name, last_name, company_name, job_title, campaign, status, payload\) | +| `campaigns` | array | List of campaigns \(id, name, status, daily_limit\) | +| `campaign` | json | Campaign details \(id, name, status, daily_limit, daily_max_leads, open_tracking\) | +| `emails` | array | List of emails \(id, subject, from_address_email, lead, thread_id\) | +| `email` | json | Email details \(id, subject, from_address_email, to_address_email_list, thread_id, content_preview\) | +| `lead_lists` | array | List of lead lists \(id, name, has_enrichment_task, timestamp_created\) | +| `lead_list` | json | Lead list details \(id, organization_id, has_enrichment_task, owned_by, name, timestamp_created\) | | `count` | number | Returned or affected record count | | `next_starting_after` | string | Cursor for the next page | | `id` | string | Record ID | @@ -310,14 +315,14 @@ Updates documented Instantly V2 campaign fields. | Parameter | Type | Description | | --------- | ---- | ----------- | -| `leads` | array | List of leads | -| `lead` | json | Lead details | -| `campaigns` | array | List of campaigns | -| `campaign` | json | Campaign details | -| `emails` | array | List of emails | -| `email` | json | Email details | -| `lead_lists` | array | List of lead lists | -| `lead_list` | json | Lead list details | +| `leads` | array | List of leads \(id, email, first_name, last_name, campaign, status\) | +| `lead` | json | Lead details \(id, email, first_name, last_name, company_name, job_title, campaign, status, payload\) | +| `campaigns` | array | List of campaigns \(id, name, status, daily_limit\) | +| `campaign` | json | Campaign details \(id, name, status, daily_limit, daily_max_leads, open_tracking\) | +| `emails` | array | List of emails \(id, subject, from_address_email, lead, thread_id\) | +| `email` | json | Email details \(id, subject, from_address_email, to_address_email_list, thread_id, content_preview\) | +| `lead_lists` | array | List of lead lists \(id, name, has_enrichment_task, timestamp_created\) | +| `lead_list` | json | Lead list details \(id, organization_id, has_enrichment_task, owned_by, name, timestamp_created\) | | `count` | number | Returned or affected record count | | `next_starting_after` | string | Cursor for the next page | | `id` | string | Record ID | @@ -344,14 +349,14 @@ Activates, starts, or resumes an Instantly V2 campaign. | Parameter | Type | Description | | --------- | ---- | ----------- | -| `leads` | array | List of leads | -| `lead` | json | Lead details | -| `campaigns` | array | List of campaigns | -| `campaign` | json | Campaign details | -| `emails` | array | List of emails | -| `email` | json | Email details | -| `lead_lists` | array | List of lead lists | -| `lead_list` | json | Lead list details | +| `leads` | array | List of leads \(id, email, first_name, last_name, campaign, status\) | +| `lead` | json | Lead details \(id, email, first_name, last_name, company_name, job_title, campaign, status, payload\) | +| `campaigns` | array | List of campaigns \(id, name, status, daily_limit\) | +| `campaign` | json | Campaign details \(id, name, status, daily_limit, daily_max_leads, open_tracking\) | +| `emails` | array | List of emails \(id, subject, from_address_email, lead, thread_id\) | +| `email` | json | Email details \(id, subject, from_address_email, to_address_email_list, thread_id, content_preview\) | +| `lead_lists` | array | List of lead lists \(id, name, has_enrichment_task, timestamp_created\) | +| `lead_list` | json | Lead list details \(id, organization_id, has_enrichment_task, owned_by, name, timestamp_created\) | | `count` | number | Returned or affected record count | | `next_starting_after` | string | Cursor for the next page | | `id` | string | Record ID | @@ -380,21 +385,20 @@ Retrieves Instantly V2 Unibox emails with search and pagination filters. | `i_status` | number | No | Email interest status filter | | `eaccount` | string | No | Sending email account filter | | `lead` | string | No | Lead email address filter | -| `lead_id` | string | No | Lead ID filter | -| `is_unread` | number | No | Unread status filter | +| `is_unread` | boolean | No | Unread status filter | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `leads` | array | List of leads | -| `lead` | json | Lead details | -| `campaigns` | array | List of campaigns | -| `campaign` | json | Campaign details | -| `emails` | array | List of emails | -| `email` | json | Email details | -| `lead_lists` | array | List of lead lists | -| `lead_list` | json | Lead list details | +| `leads` | array | List of leads \(id, email, first_name, last_name, campaign, status\) | +| `lead` | json | Lead details \(id, email, first_name, last_name, company_name, job_title, campaign, status, payload\) | +| `campaigns` | array | List of campaigns \(id, name, status, daily_limit\) | +| `campaign` | json | Campaign details \(id, name, status, daily_limit, daily_max_leads, open_tracking\) | +| `emails` | array | List of emails \(id, subject, from_address_email, lead, thread_id\) | +| `email` | json | Email details \(id, subject, from_address_email, to_address_email_list, thread_id, content_preview\) | +| `lead_lists` | array | List of lead lists \(id, name, has_enrichment_task, timestamp_created\) | +| `lead_list` | json | Lead list details \(id, organization_id, has_enrichment_task, owned_by, name, timestamp_created\) | | `count` | number | Returned or affected record count | | `next_starting_after` | string | Cursor for the next page | | `id` | string | Record ID | @@ -426,14 +430,14 @@ Sends an Instantly V2 reply to an existing Unibox email. | Parameter | Type | Description | | --------- | ---- | ----------- | -| `leads` | array | List of leads | -| `lead` | json | Lead details | -| `campaigns` | array | List of campaigns | -| `campaign` | json | Campaign details | -| `emails` | array | List of emails | -| `email` | json | Email details | -| `lead_lists` | array | List of lead lists | -| `lead_list` | json | Lead list details | +| `leads` | array | List of leads \(id, email, first_name, last_name, campaign, status\) | +| `lead` | json | Lead details \(id, email, first_name, last_name, company_name, job_title, campaign, status, payload\) | +| `campaigns` | array | List of campaigns \(id, name, status, daily_limit\) | +| `campaign` | json | Campaign details \(id, name, status, daily_limit, daily_max_leads, open_tracking\) | +| `emails` | array | List of emails \(id, subject, from_address_email, lead, thread_id\) | +| `email` | json | Email details \(id, subject, from_address_email, to_address_email_list, thread_id, content_preview\) | +| `lead_lists` | array | List of lead lists \(id, name, has_enrichment_task, timestamp_created\) | +| `lead_list` | json | Lead list details \(id, organization_id, has_enrichment_task, owned_by, name, timestamp_created\) | | `count` | number | Returned or affected record count | | `next_starting_after` | string | Cursor for the next page | | `id` | string | Record ID | @@ -463,14 +467,14 @@ Retrieves Instantly V2 lead lists with search and pagination filters. | Parameter | Type | Description | | --------- | ---- | ----------- | -| `leads` | array | List of leads | -| `lead` | json | Lead details | -| `campaigns` | array | List of campaigns | -| `campaign` | json | Campaign details | -| `emails` | array | List of emails | -| `email` | json | Email details | -| `lead_lists` | array | List of lead lists | -| `lead_list` | json | Lead list details | +| `leads` | array | List of leads \(id, email, first_name, last_name, campaign, status\) | +| `lead` | json | Lead details \(id, email, first_name, last_name, company_name, job_title, campaign, status, payload\) | +| `campaigns` | array | List of campaigns \(id, name, status, daily_limit\) | +| `campaign` | json | Campaign details \(id, name, status, daily_limit, daily_max_leads, open_tracking\) | +| `emails` | array | List of emails \(id, subject, from_address_email, lead, thread_id\) | +| `email` | json | Email details \(id, subject, from_address_email, to_address_email_list, thread_id, content_preview\) | +| `lead_lists` | array | List of lead lists \(id, name, has_enrichment_task, timestamp_created\) | +| `lead_list` | json | Lead list details \(id, organization_id, has_enrichment_task, owned_by, name, timestamp_created\) | | `count` | number | Returned or affected record count | | `next_starting_after` | string | Cursor for the next page | | `id` | string | Record ID | @@ -499,14 +503,14 @@ Creates an Instantly V2 lead list. | Parameter | Type | Description | | --------- | ---- | ----------- | -| `leads` | array | List of leads | -| `lead` | json | Lead details | -| `campaigns` | array | List of campaigns | -| `campaign` | json | Campaign details | -| `emails` | array | List of emails | -| `email` | json | Email details | -| `lead_lists` | array | List of lead lists | -| `lead_list` | json | Lead list details | +| `leads` | array | List of leads \(id, email, first_name, last_name, campaign, status\) | +| `lead` | json | Lead details \(id, email, first_name, last_name, company_name, job_title, campaign, status, payload\) | +| `campaigns` | array | List of campaigns \(id, name, status, daily_limit\) | +| `campaign` | json | Campaign details \(id, name, status, daily_limit, daily_max_leads, open_tracking\) | +| `emails` | array | List of emails \(id, subject, from_address_email, lead, thread_id\) | +| `email` | json | Email details \(id, subject, from_address_email, to_address_email_list, thread_id, content_preview\) | +| `lead_lists` | array | List of lead lists \(id, name, has_enrichment_task, timestamp_created\) | +| `lead_list` | json | Lead list details \(id, organization_id, has_enrichment_task, owned_by, name, timestamp_created\) | | `count` | number | Returned or affected record count | | `next_starting_after` | string | Cursor for the next page | | `id` | string | Record ID | diff --git a/apps/docs/content/docs/en/tools/meta.json b/apps/docs/content/docs/en/tools/meta.json index 4ebb74aacca..1cd443bed58 100644 --- a/apps/docs/content/docs/en/tools/meta.json +++ b/apps/docs/content/docs/en/tools/meta.json @@ -207,6 +207,7 @@ "youtube", "zendesk", "zep", - "zoom" + "zoom", + "zoominfo" ] } diff --git a/apps/docs/content/docs/en/tools/new_relic.mdx b/apps/docs/content/docs/en/tools/new_relic.mdx index 7beb793335a..587b875f972 100644 --- a/apps/docs/content/docs/en/tools/new_relic.mdx +++ b/apps/docs/content/docs/en/tools/new_relic.mdx @@ -5,7 +5,7 @@ description: Query observability data and record deployments in New Relic import { BlockInfoCard } from "@/components/ui/block-info-card" - @@ -141,3 +141,5 @@ Record a deployment change event in New Relic change tracking. | ↳ `guid` | string | Entity GUID | | ↳ `name` | string | Entity name | | `messages` | array | Messages returned by New Relic for the created change event | + + diff --git a/apps/docs/content/docs/en/tools/sixtyfour.mdx b/apps/docs/content/docs/en/tools/sixtyfour.mdx index 41a55c58463..aed853b33cc 100644 --- a/apps/docs/content/docs/en/tools/sixtyfour.mdx +++ b/apps/docs/content/docs/en/tools/sixtyfour.mdx @@ -124,5 +124,6 @@ Enrich company data with additional information and find associated people using | `structuredData` | json | Enriched company data matching the requested struct fields | | `references` | json | Source URLs and descriptions used for enrichment | | `confidenceScore` | number | Quality score for the returned data \(0-10\) | +| `orgChart` | json | Org chart returned when fullOrgChart is enabled | diff --git a/apps/docs/content/docs/en/tools/video_generator.mdx b/apps/docs/content/docs/en/tools/video_generator.mdx index b2e7c9fce54..35f8b5fd424 100644 --- a/apps/docs/content/docs/en/tools/video_generator.mdx +++ b/apps/docs/content/docs/en/tools/video_generator.mdx @@ -175,6 +175,9 @@ Generate videos using Fal.ai with access to Veo 3.1, Sora 2, Seedance 2.0, Kling | `resolution` | string | No | Video resolution \(varies by model\): 480p, 580p, 720p, 1080p, true_1080p, 1440p, 2160p, 4k | | `promptOptimizer` | boolean | No | Enable prompt optimization for MiniMax models \(default: true\) | | `generateAudio` | boolean | No | Generate native audio when supported by the selected Fal.ai model | +| `pricing` | custom | No | No description | +| `metadata` | string | No | No description | +| `rateLimit` | string | No | No description | #### Output diff --git a/apps/docs/content/docs/en/tools/wiza.mdx b/apps/docs/content/docs/en/tools/wiza.mdx index e8d775b07a1..84aa4ecefda 100644 --- a/apps/docs/content/docs/en/tools/wiza.mdx +++ b/apps/docs/content/docs/en/tools/wiza.mdx @@ -149,7 +149,7 @@ Enrich a company by name, domain, LinkedIn ID, or LinkedIn slug with detailed fi | `company_region` | string | State/region | | `company_postal_code` | string | Postal code | | `company_country` | string | Country | -| `credits` | json | Remaining API credits | +| `credits` | json | Credits deducted for this enrichment \(api_credits: \{ total, email_credits, phone_credits, scrape_credits \}\) | ### `wiza_start_individual_reveal` @@ -227,7 +227,7 @@ Retrieve the status and enriched data for an individual reveal by ID | `company_linkedin` | string | Company LinkedIn URL | | `company_location` | string | Full company location | | `company_description` | string | Company description | -| `credits` | json | Remaining credits balance | +| `credits` | json | Credits deducted for this reveal \(api_credits: \{ total, email_credits, phone_credits, scrape_credits \}\) | ### `wiza_get_credits` diff --git a/apps/docs/content/docs/en/tools/zoominfo.mdx b/apps/docs/content/docs/en/tools/zoominfo.mdx new file mode 100644 index 00000000000..5574e1ec167 --- /dev/null +++ b/apps/docs/content/docs/en/tools/zoominfo.mdx @@ -0,0 +1,203 @@ +--- +title: ZoomInfo +description: Search and enrich B2B company and contact data with ZoomInfo. +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + +{/* MANUAL-CONTENT-START:intro */} +[ZoomInfo](https://www.zoominfo.com/) is a B2B go-to-market platform with one of the largest verified databases of company and contact data. It pairs deep firmographic and contact records with buyer intent signals and company news, so your sales, marketing, and operations teams can find, prioritize, and reach the right accounts. + +With ZoomInfo, you can: + +- **Search companies**: Find organizations by name, industry, location, revenue, and employee size +- **Search contacts**: Discover people by name, job title, department, and management level +- **Enrich companies and contacts**: Resolve detailed firmographics, verified emails, and phone numbers in bulk +- **Track buyer intent**: Surface companies showing intent signals on the topics you care about +- **Monitor company news**: Pull recent articles by category, source, or date range + +In Sim, the ZoomInfo integration lets your agents power prospecting, enrichment, and account research workflows end-to-end: + +- **Search Companies**: Use `zoominfo_search_companies` to find accounts matching firmographic filters. +- **Search Contacts**: Use `zoominfo_search_contacts` to find people by role and company. Search results do not include emails or phone numbers — use Enrich Contacts for engagement data. +- **Enrich Companies**: Use `zoominfo_enrich_companies` to resolve up to 25 companies per request from a name, domain, ticker, or ZoomInfo ID. +- **Enrich Contacts**: Use `zoominfo_enrich_contacts` to resolve up to 25 contacts per request, returning verified emails, phones, and job details. +- **Search Intent**: Use `zoominfo_search_intent` to find companies researching specific topics, with signal scores and recommended contacts. +- **Search News**: Use `zoominfo_search_news` to monitor company news by category, source URL, or publish date. + +ZoomInfo authenticates with the OAuth 2.0 client credentials flow: Sim exchanges your client ID and secret for a short-lived bearer token automatically on each call, so you only ever provide your credentials. A common pattern is to search first to identify the records you want, then pass their IDs into the matching enrich tool to pull full detail efficiently. +{/* MANUAL-CONTENT-END */} + + +## Usage Instructions + +Integrates ZoomInfo into the workflow. Search companies and contacts, enrich firmographic and contact data, find intent signals, and pull news — all using the ZoomInfo GTM API. + + + +## Tools + +### `zoominfo_search_companies` + +Search the ZoomInfo company database by name, industry, location, and size. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `clientId` | string | Yes | ZoomInfo OAuth client ID | +| `clientSecret` | string | Yes | ZoomInfo OAuth client secret | +| `companyName` | string | No | Company name to search for | +| `companyWebsite` | string | No | Company website \(comma-separated for multiple\) | +| `companyTicker` | string | No | Stock ticker symbols — JSON array, comma-separated list, or single ticker. Sent to the API as an array. | +| `industryCodes` | string | No | Industry codes — JSON array or comma-separated list. Sent to the API as a comma-separated string. | +| `country` | string | No | Country name | +| `state` | string | No | State or province | +| `metroRegion` | string | No | US/Canada metro region | +| `revenueMin` | number | No | Minimum annual revenue in thousands USD | +| `revenueMax` | number | No | Maximum annual revenue in thousands USD | +| `employeeRangeMin` | number | No | Minimum employee count | +| `employeeRangeMax` | number | No | Maximum employee count | +| `excludeDefunctCompanies` | boolean | No | Exclude inactive companies | +| `page` | number | No | Page number \(1-based\) | +| `rpp` | number | No | Results per page \(1-100, default 25\) | +| `sortBy` | string | No | Field to sort by | +| `sortOrder` | string | No | Sort order \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `companies` | array | Matching companies | + +### `zoominfo_search_contacts` + +Search ZoomInfo for contacts (people) by name, job title, company, and other filters. Does not return emails or phone numbers — use Enrich Contacts for engagement data. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `clientId` | string | Yes | ZoomInfo OAuth client ID | +| `clientSecret` | string | Yes | ZoomInfo OAuth client secret | +| `firstName` | string | No | First name | +| `lastName` | string | No | Last name | +| `fullName` | string | No | Full name | +| `emailAddress` | string | No | Email address | +| `jobTitle` | string | No | Job title | +| `managementLevel` | string | No | Management level — JSON array or comma-separated list. Sent to the API as a comma-separated string. | +| `department` | string | No | Department — JSON array or comma-separated list. Sent to the API as a comma-separated string. | +| `companyId` | string | No | ZoomInfo company ID | +| `companyName` | string | No | Company name | +| `contactAccuracyScoreMin` | number | No | Minimum accuracy score \(70-99\) | +| `requiredFields` | string | No | Fields that must exist in results — JSON array or comma-separated list. Sent to the API as a comma-separated string. | +| `excludePartialProfiles` | boolean | No | Exclude partial profiles | +| `page` | number | No | Page number \(1-based\) | +| `rpp` | number | No | Results per page \(1-100, default 25\) | +| `sortBy` | string | No | Field to sort by | +| `sortOrder` | string | No | Sort order \(asc or desc\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `contacts` | array | Matching contacts \(without emails or phone numbers\) | + +### `zoominfo_enrich_companies` + +Enrich up to 25 companies in one request with detailed firmographics, industry, financials, and more. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `clientId` | string | Yes | ZoomInfo OAuth client ID | +| `clientSecret` | string | Yes | ZoomInfo OAuth client secret | +| `matchCompanyInput` | string | Yes | JSON array \(1-25 items\) of company matching criteria, e.g. \[\{"companyName":"Acme","companyWebsite":"acme.com"\}\] | +| `outputFields` | string | No | JSON array or comma-separated list of fields to return \(e.g. \["id","name","website","revenue","employeeCount"\]\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `results` | array | Enrichment results, one per input with match status and attributes | + +### `zoominfo_enrich_contacts` + +Enrich up to 25 contacts in one request with verified emails, phone numbers, job details, and more. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `clientId` | string | Yes | ZoomInfo OAuth client ID | +| `clientSecret` | string | Yes | ZoomInfo OAuth client secret | +| `matchPersonInput` | string | Yes | JSON array \(1-25 items\) of contact matching criteria, e.g. \[\{"firstName":"Jane","lastName":"Doe","companyName":"Acme"\}\] | +| `outputFields` | string | No | JSON array or comma-separated list of fields to return \(e.g. \["id","firstName","email","phone","jobTitle"\]\) | +| `requiredFields` | string | No | JSON array or comma-separated list of fields that must exist in results \(e.g. \["email"\]\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `results` | array | Enrichment results, one per input with match status and attributes | + +### `zoominfo_search_intent` + +Search for companies showing intent signals on specific topics. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `clientId` | string | Yes | ZoomInfo OAuth client ID | +| `clientSecret` | string | Yes | ZoomInfo OAuth client secret | +| `topics` | string | Yes | Up to 50 intent topics as JSON array or comma-separated list \(e.g. \["CRM Software","Marketing Automation"\]\) | +| `signalStartDate` | string | No | Earliest signal date \(YYYY-MM-DD\) | +| `signalEndDate` | string | No | Latest signal date \(YYYY-MM-DD\) | +| `signalScoreMin` | number | No | Minimum signal score \(60-100\) | +| `signalScoreMax` | number | No | Maximum signal score \(60-100\) | +| `audienceStrengthMin` | string | No | Minimum audience strength \(A-E, A is largest\) | +| `audienceStrengthMax` | string | No | Maximum audience strength \(A-E, A is largest\) | +| `findRecommendedContacts` | boolean | No | Include recommended contacts \(default true\) | +| `country` | string | No | Country filter | +| `state` | string | No | State filter | +| `industryCodes` | string | No | Industry codes — JSON array or comma-separated list. Sent to the API as a comma-separated string. | +| `page` | number | No | Page number \(1-based\) | +| `rpp` | number | No | Results per page \(1-100, default 25\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `signals` | array | Intent signals with topic, score, audience strength, and company | + +### `zoominfo_search_news` + +Search ZoomInfo news articles by category, URL, or date range. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `clientId` | string | Yes | ZoomInfo OAuth client ID | +| `clientSecret` | string | Yes | ZoomInfo OAuth client secret | +| `categories` | string | No | News categories as JSON array or comma-separated list | +| `url` | string | No | News source URLs as JSON array or comma-separated list | +| `pageDateMin` | string | No | Earliest publish date \(YYYY-MM-DD\) | +| `pageDateMax` | string | No | Latest publish date \(YYYY-MM-DD\) | +| `page` | number | No | Page number \(1-based\) | +| `rpp` | number | No | Results per page \(1-100, default 25\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `articles` | array | News articles matching the filters | + + diff --git a/apps/docs/content/docs/en/triggers/hubspot.mdx b/apps/docs/content/docs/en/triggers/hubspot.mdx index 3e8e1c6708c..532368cacc8 100644 --- a/apps/docs/content/docs/en/triggers/hubspot.mdx +++ b/apps/docs/content/docs/en/triggers/hubspot.mdx @@ -16,34 +16,42 @@ All triggers below are **polling-based** — they check for new data on a schedu ## Triggers -### HubSpot Trigger +### HubSpot CRM Trigger -Triggers when a HubSpot record (contact, company, deal, ticket, or custom object) is created or updated +Triggers when HubSpot CRM records (contacts, companies, deals, tickets, custom objects) are created or updated, or when contacts join a list #### Configuration | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `triggerCredentials` | string | Yes | Connect a HubSpot account so Sim can poll your CRM on your behalf. | -| `objectType` | string | Yes | Which HubSpot CRM object to watch. Pick | -| `customObjectTypeId` | string | Yes | HubSpot custom object type ID \(e.g. | -| `eventType` | string | Yes | Created fires once per new record. Updated fires whenever the record changes \(and on creation\). | -| `properties` | string | No | Comma- or newline-separated list of HubSpot property names to include on each record. Leave empty to use sensible defaults. Sim always includes the timestamp properties Sim needs internally, regardless of this list. | -| `filterPropertyName` | string | No | Only emit records where this property equals the value below. Leave both fields empty to emit every change. | -| `filterPropertyValue` | string | No | Value the filter property must match \(exact match, case-sensitive\). | -| `maxRecordsPerPoll` | string | No | Cap on records emitted per poll \(default 50, max 1000\). Excess rolls over to the next poll. | +| `objectType` | string | Yes | What you want to watch. | +| `customObjectTypeId` | string | No | HubSpot custom object type ID \(e.g. | +| `listId` | string | No | The HubSpot list to watch for new members. | +| `eventType` | string | No | Created fires once per new record. Updated fires on any modification. Property Changed fires only when the chosen property changes value. | +| `targetPropertyName` | string | No | Fires only when this specific property changes value on a record. | +| `properties` | string | No | Properties to include on each record. Leave empty to use sensible defaults. Sim always includes the timestamps it needs internally. | +| `pipelineId` | string | No | Restrict to a single pipeline. | +| `stageId` | string | No | Restrict to a single stage within the selected pipeline. | +| `ownerId` | string | No | Restrict to records owned by a specific HubSpot user. | +| `filters` | string | No | JSON array of HubSpot search filters, AND-combined. Each item: \{ | +| `maxRecordsPerPoll` | string | No | Soft cap on records emitted per poll \(default 50, max 1000\). Excess rolls over to the next poll. | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `objectType` | string | HubSpot object type that fired the trigger \(contact, company, deal, ticket, or custom object type ID\) | -| `eventType` | string | Event type that fired the trigger \(created or updated\) | -| `objectId` | string | HubSpot ID of the affected record | -| `occurredAt` | string | ISO timestamp of the create or update on the record \(sourced from the relevant HubSpot timestamp property\) | -| `properties` | json | HubSpot properties returned for the record \(object of property name to value\) | +| `objectType` | string | HubSpot object type \(contact, company, deal, ticket, custom object id, or list_membership\) | +| `eventType` | string | Event type \(created, updated, property_changed, or joined\) | +| `objectId` | string | HubSpot ID of the affected record \(or contact id for list memberships\) | +| `occurredAt` | string | ISO timestamp of when the change happened in HubSpot | +| `properties` | json | HubSpot properties on the record as a key-value object \(property internal name → value\). Default keys per object type \(override via "Properties to Fetch"\): Contact → firstname, lastname, email, phone, company, lifecyclestage, hs_lead_status, hubspot_owner_id, createdate, lastmodifieddate. Company → name, domain, industry, lifecyclestage, hubspot_owner_id, createdate, hs_lastmodifieddate. Deal → dealname, amount, dealstage, pipeline, closedate, hubspot_owner_id, createdate, hs_lastmodifieddate. Ticket → subject, content, hs_pipeline, hs_pipeline_stage, hs_ticket_priority, hubspot_owner_id, createdate, hs_lastmodifieddate. Custom and user-requested properties appear keyed by their HubSpot internal name. | | `createdAt` | string | ISO timestamp when the record was created in HubSpot | | `updatedAt` | string | ISO timestamp when the record was last updated in HubSpot | | `archived` | boolean | Whether the record is archived | +| `propertyName` | string | Name of the property that changed \(property_changed events only\) | +| `propertyValue` | string | New value of the changed property \(property_changed events only\) | +| `previousValue` | string | Previous value before the change \(property_changed events only\) | +| `listId` | string | HubSpot list ID \(list_membership events only\) | | `timestamp` | string | ISO timestamp when Sim emitted the event | diff --git a/apps/sim/app/(landing)/integrations/data/icon-mapping.ts b/apps/sim/app/(landing)/integrations/data/icon-mapping.ts index 8db9d2f447b..f6dd99d8314 100644 --- a/apps/sim/app/(landing)/integrations/data/icon-mapping.ts +++ b/apps/sim/app/(landing)/integrations/data/icon-mapping.ts @@ -208,6 +208,7 @@ import { ZendeskIcon, ZepIcon, ZoomIcon, + ZoomInfoIcon, } from '@/components/icons' type IconComponent = ComponentType> @@ -418,4 +419,5 @@ export const blockTypeToIconMap: Record = { zendesk: ZendeskIcon, zep: ZepIcon, zoom: ZoomIcon, + zoominfo: ZoomInfoIcon, } diff --git a/apps/sim/app/(landing)/integrations/data/integrations.json b/apps/sim/app/(landing)/integrations/data/integrations.json index 0832536e1de..8508cd87cf4 100644 --- a/apps/sim/app/(landing)/integrations/data/integrations.json +++ b/apps/sim/app/(landing)/integrations/data/integrations.json @@ -214,7 +214,7 @@ "name": "AgentPhone", "description": "Provision numbers, send SMS and iMessage, and place voice calls with AgentPhone", "longDescription": "Give your workflow a phone. Provision SMS- and voice-enabled numbers, send messages and tapback reactions, place outbound voice calls, manage conversations and contacts, and track usage — all through a single AgentPhone API key.", - "bgColor": "linear-gradient(135deg, #1a1a1a 0%, #0a2a14 100%)", + "bgColor": "#000000", "iconName": "AgentPhoneIcon", "docsUrl": "https://docs.sim.ai/tools/agentphone", "operations": [ @@ -3730,7 +3730,7 @@ "type": "elevenlabs", "slug": "elevenlabs", "name": "ElevenLabs", - "description": "Convert TTS using ElevenLabs", + "description": "Convert text to speech with ElevenLabs", "longDescription": "Integrate ElevenLabs into the workflow. Can convert text to speech.", "bgColor": "#181C1E", "iconName": "ElevenLabsIcon", @@ -5785,7 +5785,7 @@ "slug": "google-slides", "name": "Google Slides", "description": "Read, write, and create presentations", - "longDescription": "Integrate Google Slides into the workflow. Can read, write, create presentations, replace text, add slides, add images, get thumbnails, get page details, delete objects, duplicate objects, reorder slides, create tables, create shapes, and insert text.", + "longDescription": "Build, edit, and export branded Google Slides presentations end-to-end. Copy a template, replace text and image tokens, embed Sheets charts, style text and shapes with brand fonts and colors, manage tables and layouts, group elements, run atomic batch updates, and export to PDF or PPTX.", "bgColor": "#E0E0E0", "iconName": "GoogleSlidesIcon", "docsUrl": "https://docs.sim.ai/tools/google_slides", @@ -5802,10 +5802,34 @@ "name": "Create Presentation", "description": "Create a new Google Slides presentation" }, + { + "name": "Copy Presentation", + "description": "Copy a template presentation in Drive to a new file. Use this before merging data so the original template is never modified." + }, + { + "name": "Export Presentation", + "description": "Export a presentation to PDF, PPTX, ODP, TXT, PNG, JPEG, or SVG via the Drive export endpoint. Stores the exported file as an execution file when execution context is available." + }, + { + "name": "Batch Update (Raw)", + "description": "Run a raw Slides API batchUpdate with a list of Request objects. Use this when the higher-level tools do not cover an operation, or to bundle multiple operations into a single atomic batch (all-or-nothing)." + }, { "name": "Replace All Text", "description": "Find and replace all occurrences of text throughout a Google Slides presentation" }, + { + "name": "Replace All Shapes With Image", + "description": "Find every shape whose text matches the given token (e.g. {{cover-image}}) and replace it with an image, preserving the shape" + }, + { + "name": "Replace Image", + "description": "Replace the source of an existing image with a new image URL, preserving the image" + }, + { + "name": "Update Image Properties", + "description": "Update image properties — brightness, contrast, transparency, crop, outline, link — on an existing image." + }, { "name": "Add Slide", "description": "Add a new slide to a Google Slides presentation with a specified layout" @@ -5842,12 +5866,140 @@ "name": "Create Shape", "description": "Create a shape (rectangle, ellipse, text box, arrow, etc.) on a slide in a Google Slides presentation" }, + { + "name": "Create Line", + "description": "Create a line or connector on a slide." + }, { "name": "Insert Text", "description": "Insert text into a shape or table cell in a Google Slides presentation. Use this to add text to text boxes, shapes, or table cells." + }, + { + "name": "Delete Text", + "description": "Delete text from a shape or table cell. Use range type ALL to clear all text, or FIXED_RANGE / FROM_START_INDEX to delete a specific span." + }, + { + "name": "Update Text Style", + "description": "Update the styling of text in a shape or table cell (bold, italic, font family, font size, foreground/background color, link, etc.). Only the fields you set are applied." + }, + { + "name": "Update Paragraph Style", + "description": "Update paragraph styling — alignment, line spacing, indents, space above/below — for text in a shape or table cell." + }, + { + "name": "Create Paragraph Bullets", + "description": "Convert paragraphs in a shape or table cell into a bulleted or numbered list using a Google Slides bullet preset." + }, + { + "name": "Delete Paragraph Bullets", + "description": "Remove bullets/numbering from paragraphs in a shape or table cell." + }, + { + "name": "Update Shape Properties", + "description": "Update a shape" + }, + { + "name": "Update Page Properties", + "description": "Update slide/page background — solid color or stretched picture — and other page properties." + }, + { + "name": "Update Slide Properties", + "description": "Update slide-specific properties such as whether the slide is skipped during presentation." + }, + { + "name": "Update Alt Text", + "description": "Set the accessibility title and/or description (alt text) of a page element such as an image, shape, or group." + }, + { + "name": "Update Element Transform", + "description": "Move, resize, scale, or shear a page element. Translate is specified in points; applyMode controls whether the transform is absolute (default) or relative (multiplied with the current transform)." + }, + { + "name": "Update Z-Order", + "description": "Bring elements to front, send to back, or step them one layer forward/backward." + }, + { + "name": "Group Objects", + "description": "Group two or more page elements on the same slide into a single object group." + }, + { + "name": "Ungroup Objects", + "description": "Ungroup one or more object groups, releasing their children back to the slide." + }, + { + "name": "Update Line Properties", + "description": "Update line appearance — color, weight, dash style, arrows, link." + }, + { + "name": "Update Line Category", + "description": "Change a connector line" + }, + { + "name": "Reroute Line", + "description": "Reroute a connector line so it efficiently connects its endpoint shapes — useful after moving the shapes the line connects." + }, + { + "name": "Insert Table Rows", + "description": "Insert one or more rows into a table, above or below a reference cell." + }, + { + "name": "Insert Table Columns", + "description": "Insert one or more columns into a table, left or right of a reference cell." + }, + { + "name": "Delete Table Row", + "description": "Delete the row containing the reference cell from a table." + }, + { + "name": "Delete Table Column", + "description": "Delete the column containing the reference cell from a table." + }, + { + "name": "Merge Table Cells", + "description": "Merge a rectangular range of table cells into a single cell. The range starts at (rowIndex, columnIndex) and covers rowSpan × columnSpan cells." + }, + { + "name": "Unmerge Table Cells", + "description": "Unmerge any merged cells within the given table range." + }, + { + "name": "Update Table Cell Properties", + "description": "Update background fill and content alignment for a range of table cells." + }, + { + "name": "Update Table Border Properties", + "description": "Update border color, weight, and dash style for a position (e.g. ALL, INNER, OUTER) in a table range." + }, + { + "name": "Update Table Column Properties", + "description": "Update column widths and other column-level table properties." + }, + { + "name": "Update Table Row Properties", + "description": "Update minimum row heights and other row-level table properties." + }, + { + "name": "Embed Sheets Chart", + "description": "Embed a chart from a Google Sheets spreadsheet onto a slide. LINKED charts can be refreshed; NOT_LINKED_IMAGE inserts a static image of the chart." + }, + { + "name": "Refresh Sheets Chart", + "description": "Refresh an embedded linked Sheets chart so it reflects the latest spreadsheet data." + }, + { + "name": "Replace All Shapes With Sheets Chart", + "description": "Find every shape matching a text token (e.g. {{revenue-chart}}) and replace each with the same embedded Sheets chart, preserving the shape" + }, + { + "name": "Embed Video", + "description": "Embed a YouTube or Google Drive video on a slide." + }, + { + "name": "Update Video Properties", + "description": "Update video playback options (autoPlay, mute, start/end) and outline." } ], - "operationCount": 14, + "operationCount": 52, "triggers": [], "triggerCount": 0, "authType": "oauth", @@ -6417,7 +6569,7 @@ "slug": "hubspot", "name": "HubSpot", "description": "Interact with HubSpot CRM or trigger workflows from HubSpot events", - "longDescription": "Integrate HubSpot into your workflow. Manage contacts, companies, deals, tickets, and other CRM objects with powerful automation capabilities. Can be used in trigger mode to start workflows when contacts are created, deleted, or updated.", + "longDescription": "Integrate HubSpot into your workflow. Manage contacts, companies, deals, tickets, and other CRM objects with powerful automation capabilities. Can be used in trigger mode to start workflows when records are created, updated, a specific property changes, or a contact joins a list.", "bgColor": "#FF7A59", "iconName": "HubspotIcon", "docsUrl": "https://docs.sim.ai/tools/hubspot", @@ -6542,142 +6694,12 @@ "operationCount": 29, "triggers": [ { - "id": "hubspot_contact_created", - "name": "HubSpot Contact Created", - "description": "Trigger workflow when a new contact is created in HubSpot" - }, - { - "id": "hubspot_contact_deleted", - "name": "HubSpot Contact Deleted", - "description": "Trigger workflow when a contact is deleted in HubSpot" - }, - { - "id": "hubspot_contact_merged", - "name": "HubSpot Contact Merged", - "description": "Trigger workflow when contacts are merged in HubSpot" - }, - { - "id": "hubspot_contact_privacy_deleted", - "name": "HubSpot Contact Privacy Deleted", - "description": "Trigger workflow when a contact is deleted for privacy compliance (GDPR, CCPA, etc.) in HubSpot" - }, - { - "id": "hubspot_contact_property_changed", - "name": "HubSpot Contact Property Changed", - "description": "Trigger workflow when any property of a contact is updated in HubSpot" - }, - { - "id": "hubspot_contact_restored", - "name": "HubSpot Contact Restored", - "description": "Trigger workflow when a deleted contact is restored in HubSpot" - }, - { - "id": "hubspot_company_created", - "name": "HubSpot Company Created", - "description": "Trigger workflow when a new company is created in HubSpot" - }, - { - "id": "hubspot_company_deleted", - "name": "HubSpot Company Deleted", - "description": "Trigger workflow when a company is deleted in HubSpot" - }, - { - "id": "hubspot_company_merged", - "name": "HubSpot Company Merged", - "description": "Trigger workflow when companies are merged in HubSpot" - }, - { - "id": "hubspot_company_property_changed", - "name": "HubSpot Company Property Changed", - "description": "Trigger workflow when any property of a company is updated in HubSpot" - }, - { - "id": "hubspot_company_restored", - "name": "HubSpot Company Restored", - "description": "Trigger workflow when a deleted company is restored in HubSpot" - }, - { - "id": "hubspot_conversation_creation", - "name": "HubSpot Conversation Creation", - "description": "Trigger workflow when a new conversation is created in HubSpot" - }, - { - "id": "hubspot_conversation_deletion", - "name": "HubSpot Conversation Deletion", - "description": "Trigger workflow when a conversation is deleted in HubSpot" - }, - { - "id": "hubspot_conversation_new_message", - "name": "HubSpot Conversation New Message", - "description": "Trigger workflow when a new message is added to a conversation in HubSpot" - }, - { - "id": "hubspot_conversation_privacy_deletion", - "name": "HubSpot Conversation Privacy Deletion", - "description": "Trigger workflow when a conversation is deleted for privacy compliance (GDPR, CCPA, etc.) in HubSpot" - }, - { - "id": "hubspot_conversation_property_changed", - "name": "HubSpot Conversation Property Changed", - "description": "Trigger workflow when any property of a conversation is updated in HubSpot" - }, - { - "id": "hubspot_deal_created", - "name": "HubSpot Deal Created", - "description": "Trigger workflow when a new deal is created in HubSpot" - }, - { - "id": "hubspot_deal_deleted", - "name": "HubSpot Deal Deleted", - "description": "Trigger workflow when a deal is deleted in HubSpot" - }, - { - "id": "hubspot_deal_merged", - "name": "HubSpot Deal Merged", - "description": "Trigger workflow when deals are merged in HubSpot" - }, - { - "id": "hubspot_deal_property_changed", - "name": "HubSpot Deal Property Changed", - "description": "Trigger workflow when any property of a deal is updated in HubSpot" - }, - { - "id": "hubspot_deal_restored", - "name": "HubSpot Deal Restored", - "description": "Trigger workflow when a deleted deal is restored in HubSpot" - }, - { - "id": "hubspot_ticket_created", - "name": "HubSpot Ticket Created", - "description": "Trigger workflow when a new ticket is created in HubSpot" - }, - { - "id": "hubspot_ticket_deleted", - "name": "HubSpot Ticket Deleted", - "description": "Trigger workflow when a ticket is deleted in HubSpot" - }, - { - "id": "hubspot_ticket_merged", - "name": "HubSpot Ticket Merged", - "description": "Trigger workflow when tickets are merged in HubSpot" - }, - { - "id": "hubspot_ticket_property_changed", - "name": "HubSpot Ticket Property Changed", - "description": "Trigger workflow when any property of a ticket is updated in HubSpot" - }, - { - "id": "hubspot_ticket_restored", - "name": "HubSpot Ticket Restored", - "description": "Trigger workflow when a deleted ticket is restored in HubSpot" - }, - { - "id": "hubspot_webhook", - "name": "HubSpot Webhook (All Events)", - "description": "Trigger workflow on any HubSpot webhook event" + "id": "hubspot_poller", + "name": "HubSpot CRM Trigger", + "description": "Triggers when HubSpot CRM records (contacts, companies, deals, tickets, custom objects) are created or updated, or when contacts join a list" } ], - "triggerCount": 27, + "triggerCount": 1, "authType": "oauth", "category": "tools", "integrationTypes": ["crm", "analytics", "customer-support", "sales"], @@ -15257,5 +15279,48 @@ "category": "tools", "integrationTypes": ["communication", "productivity"], "tags": ["meeting", "calendar", "scheduling"] + }, + { + "type": "zoominfo", + "slug": "zoominfo", + "name": "ZoomInfo", + "description": "Search and enrich B2B company and contact data with ZoomInfo.", + "longDescription": "Integrates ZoomInfo into the workflow. Search companies and contacts, enrich firmographic and contact data, find intent signals, and pull news — all using the ZoomInfo GTM API.", + "bgColor": "#EA1B15", + "iconName": "ZoomInfoIcon", + "docsUrl": "https://docs.sim.ai/tools/zoominfo", + "operations": [ + { + "name": "Search Companies", + "description": "Search the ZoomInfo company database by name, industry, location, and size." + }, + { + "name": "Search Contacts", + "description": "Search ZoomInfo for contacts (people) by name, job title, company, and other filters. Does not return emails or phone numbers — use Enrich Contacts for engagement data." + }, + { + "name": "Enrich Companies", + "description": "Enrich up to 25 companies in one request with detailed firmographics, industry, financials, and more." + }, + { + "name": "Enrich Contacts", + "description": "Enrich up to 25 contacts in one request with verified emails, phone numbers, job details, and more." + }, + { + "name": "Search Intent", + "description": "Search for companies showing intent signals on specific topics." + }, + { + "name": "Search News", + "description": "Search ZoomInfo news articles by category, URL, or date range." + } + ], + "operationCount": 6, + "triggers": [], + "triggerCount": 0, + "authType": "none", + "category": "tools", + "integrationTypes": ["sales"], + "tags": ["enrichment", "sales-engagement"] } ] diff --git a/apps/sim/app/api/tools/zoominfo/proxy/route.ts b/apps/sim/app/api/tools/zoominfo/proxy/route.ts new file mode 100644 index 00000000000..510fe9b2d77 --- /dev/null +++ b/apps/sim/app/api/tools/zoominfo/proxy/route.ts @@ -0,0 +1,125 @@ +import { createLogger } from '@sim/logger' +import { toError } from '@sim/utils/errors' +import { type NextRequest, NextResponse } from 'next/server' +import { getValidationErrorMessage, isZodError } 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 { + assertSafeZoomInfoUrl, + extractZoomInfoError, + fetchZoomInfoAccessToken, + ZOOMINFO_API_BASE, + ZOOMINFO_OUTBOUND_FETCH_TIMEOUT_MS, + type ZoomInfoProxyRequest, + ZoomInfoProxyRequestSchema, +} from '@/app/api/tools/zoominfo/shared' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('ZoomInfoProxyAPI') + +function buildApiUrl(req: ZoomInfoProxyRequest): string { + const subPath = req.path.startsWith('/') ? req.path : `/${req.path}` + const url = `${ZOOMINFO_API_BASE}${subPath}` + + if (!req.query || Object.keys(req.query).length === 0) { + return url + } + const search = new URLSearchParams() + for (const [key, value] of Object.entries(req.query)) { + if (value === undefined || value === null) continue + search.append(key, String(value)) + } + const queryString = search.toString() + if (!queryString) return url + return url.includes('?') ? `${url}&${queryString}` : `${url}?${queryString}` +} + +interface Invocation { + status: number + body: unknown +} + +async function callZoomInfo(req: ZoomInfoProxyRequest, accessToken: string): Promise { + const url = assertSafeZoomInfoUrl(buildApiUrl(req), 'apiUrl').toString() + const hasBody = req.body !== undefined && req.body !== null + const headers: Record = { + Authorization: `Bearer ${accessToken}`, + Accept: 'application/json', + } + if (hasBody) headers['Content-Type'] = 'application/json' + + const response = await secureFetchWithValidation( + url, + { + method: req.method, + headers, + body: hasBody + ? typeof req.body === 'string' + ? req.body + : JSON.stringify(req.body) + : undefined, + timeout: ZOOMINFO_OUTBOUND_FETCH_TIMEOUT_MS, + }, + 'apiUrl' + ) + + const raw = await response.text() + let parsed: unknown = null + if (raw.length > 0) { + try { + parsed = JSON.parse(raw) + } catch { + parsed = raw + } + } + return { status: response.status, body: parsed } +} + +export const POST = withRouteHandler(async (request: NextRequest) => { + const requestId = generateRequestId() + + try { + const authResult = await checkInternalAuth(request, { requireWorkflowId: false }) + if (!authResult.success) { + logger.warn(`[${requestId}] Unauthorized ZoomInfo proxy request: ${authResult.error}`) + return NextResponse.json( + { success: false, error: authResult.error || 'Authentication required' }, + { status: 401 } + ) + } + + // boundary-raw-json: internal proxy envelope validated by ZoomInfoProxyRequestSchema below; not a public boundary + const json = await request.json() + const proxyReq = ZoomInfoProxyRequestSchema.parse(json) + + const accessToken = await fetchZoomInfoAccessToken(proxyReq, requestId) + const invocation = await callZoomInfo(proxyReq, accessToken) + + if (invocation.status >= 200 && invocation.status < 300) { + const data = invocation.status === 204 ? null : invocation.body + return NextResponse.json({ success: true, output: { status: invocation.status, data } }) + } + + const message = extractZoomInfoError(invocation.body, invocation.status) + logger.warn( + `[${requestId}] ZoomInfo API error (${invocation.status}) ${proxyReq.path}: ${message}` + ) + return NextResponse.json( + { success: false, error: message, status: invocation.status }, + { status: invocation.status } + ) + } catch (error) { + if (isZodError(error)) { + logger.warn(`[${requestId}] Validation error:`, error.issues) + return NextResponse.json( + { success: false, error: getValidationErrorMessage(error, 'Validation failed') }, + { status: 400 } + ) + } + logger.error(`[${requestId}] Unexpected ZoomInfo proxy error:`, error) + return NextResponse.json({ success: false, error: toError(error).message }, { status: 500 }) + } +}) diff --git a/apps/sim/app/api/tools/zoominfo/shared.ts b/apps/sim/app/api/tools/zoominfo/shared.ts new file mode 100644 index 00000000000..0b9f4674f06 --- /dev/null +++ b/apps/sim/app/api/tools/zoominfo/shared.ts @@ -0,0 +1,204 @@ +import { createHash } from 'node:crypto' +import { createLogger } from '@sim/logger' +import { z } from 'zod' +import { secureFetchWithValidation } from '@/lib/core/security/input-validation.server' + +const logger = createLogger('ZoomInfoShared') + +export const ZOOMINFO_API_BASE = 'https://api.zoominfo.com/gtm' +export const ZOOMINFO_TOKEN_URL = `${ZOOMINFO_API_BASE}/oauth/v1/token` +export const ZOOMINFO_OUTBOUND_FETCH_TIMEOUT_MS = 30_000 + +export const ZoomInfoAuthSchema = z.object({ + clientId: z.string().min(1, 'clientId is required'), + clientSecret: z.string().min(1, 'clientSecret is required'), +}) + +export type ZoomInfoAuth = z.infer + +export const ZoomInfoHttpMethod = z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']) + +export const ZoomInfoProxyPath = z + .string() + .min(1, 'path is required') + .refine( + (p) => + !p.split(/[/\\]/).some((seg) => seg === '..' || seg === '.') && + !p.includes('#') && + !/%(?:2[eEfF]|5[cC]|23)/.test(p), + { + message: + 'path must not contain ".." or "." segments, "#", or percent-encoded path/fragment characters', + } + ) + +export const ZoomInfoProxyRequestSchema = ZoomInfoAuthSchema.extend({ + path: ZoomInfoProxyPath, + method: ZoomInfoHttpMethod.default('POST'), + query: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])).optional(), + body: z.unknown().optional(), +}) + +export type ZoomInfoProxyRequest = z.infer + +const FORBIDDEN_HOSTS = new Set([ + 'localhost', + '0.0.0.0', + '127.0.0.1', + '169.254.169.254', + 'metadata.google.internal', + 'metadata', + '[::1]', + '[::]', +]) + +function isPrivateIPv4(host: string): boolean { + const match = host.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/) + if (!match) return false + const octets = match.slice(1, 5).map(Number) as [number, number, number, number] + if (octets.some((o) => o < 0 || o > 255)) return false + const [a, b] = octets + if (a === 10) return true + if (a === 172 && b >= 16 && b <= 31) return true + if (a === 192 && b === 168) return true + if (a === 127) return true + if (a === 169 && b === 254) return true + if (a === 0) return true + return false +} + +export function assertSafeZoomInfoUrl(rawUrl: string, label: string): URL { + let parsed: URL + try { + parsed = new URL(rawUrl) + } catch { + throw new Error(`${label} must be a valid URL`) + } + if (parsed.protocol !== 'https:') { + throw new Error(`${label} must use https://`) + } + const host = parsed.hostname.toLowerCase() + if (FORBIDDEN_HOSTS.has(host)) { + throw new Error(`${label} host is not allowed`) + } + if (isPrivateIPv4(host)) { + throw new Error(`${label} host is not allowed (private/loopback range)`) + } + if (host !== 'api.zoominfo.com') { + throw new Error(`${label} host must be api.zoominfo.com`) + } + return parsed +} + +interface CachedToken { + accessToken: string + expiresAt: number +} + +const TOKEN_CACHE = new Map() +const TOKEN_CACHE_MAX_ENTRIES = 500 +const TOKEN_SAFETY_WINDOW_MS = 60_000 + +function tokenCacheKey(auth: ZoomInfoAuth): string { + const secretHash = createHash('sha256').update(auth.clientSecret).digest('hex').slice(0, 16) + return `${auth.clientId}::${secretHash}` +} + +function rememberToken(key: string, token: CachedToken): void { + if (TOKEN_CACHE.has(key)) TOKEN_CACHE.delete(key) + TOKEN_CACHE.set(key, token) + while (TOKEN_CACHE.size > TOKEN_CACHE_MAX_ENTRIES) { + const oldestKey = TOKEN_CACHE.keys().next().value + if (oldestKey === undefined) break + TOKEN_CACHE.delete(oldestKey) + } +} + +export async function fetchZoomInfoAccessToken( + auth: ZoomInfoAuth, + requestId: string +): Promise { + const cacheKey = tokenCacheKey(auth) + const cached = TOKEN_CACHE.get(cacheKey) + if (cached && cached.expiresAt - TOKEN_SAFETY_WINDOW_MS > Date.now()) { + return cached.accessToken + } + + const tokenUrl = assertSafeZoomInfoUrl(ZOOMINFO_TOKEN_URL, 'tokenUrl').toString() + const basic = Buffer.from(`${auth.clientId}:${auth.clientSecret}`).toString('base64') + + const params = new URLSearchParams() + params.set('grant_type', 'client_credentials') + + const response = await secureFetchWithValidation( + tokenUrl, + { + method: 'POST', + headers: { + Authorization: `Basic ${basic}`, + 'Content-Type': 'application/x-www-form-urlencoded', + Accept: 'application/json', + }, + body: params.toString(), + timeout: ZOOMINFO_OUTBOUND_FETCH_TIMEOUT_MS, + }, + 'tokenUrl' + ) + + if (!response.ok) { + const text = await response.text().catch(() => '') + logger.warn(`[${requestId}] ZoomInfo token fetch failed (${response.status}): ${text}`) + throw new Error(`ZoomInfo token request failed: HTTP ${response.status}`) + } + + const data = (await response.json()) as { + access_token?: string + expires_in?: number + } + + if (!data.access_token) { + throw new Error('ZoomInfo token response missing access_token') + } + + const expiresInMs = (data.expires_in ?? 3300) * 1000 + rememberToken(cacheKey, { + accessToken: data.access_token, + expiresAt: Date.now() + expiresInMs, + }) + return data.access_token +} + +export function extractZoomInfoError(body: unknown, status: number): string { + if (body && typeof body === 'object') { + const obj = body as Record + if (obj.error && typeof obj.error === 'object') { + const eo = obj.error as Record + const message = typeof eo.message === 'string' ? eo.message : '' + const code = typeof eo.code === 'string' ? eo.code : '' + if (message) return code ? `[${code}] ${message}` : message + } + if (typeof obj.error === 'string' && obj.error.length > 0) { + const desc = typeof obj.error_description === 'string' ? `: ${obj.error_description}` : '' + return `${obj.error}${desc}` + } + if (typeof obj.message === 'string' && obj.message.length > 0) { + return obj.message + } + if (Array.isArray(obj.errors) && obj.errors.length > 0) { + return obj.errors + .map((e) => { + if (e && typeof e === 'object') { + const eo = e as Record + const title = typeof eo.title === 'string' ? eo.title : '' + const detail = typeof eo.detail === 'string' ? `: ${eo.detail}` : '' + return `${title}${detail}`.trim() + } + return String(e) + }) + .filter(Boolean) + .join('; ') + } + } + if (typeof body === 'string' && body.length > 0) return body + return `ZoomInfo request failed with HTTP ${status}` +} diff --git a/apps/sim/blocks/blocks/apollo.ts b/apps/sim/blocks/blocks/apollo.ts index 3a0aa50344a..7c0510e8899 100644 --- a/apps/sim/blocks/blocks/apollo.ts +++ b/apps/sim/blocks/blocks/apollo.ts @@ -224,11 +224,10 @@ export const ApolloBlock: BlockConfig = { required: true, }, { - id: 'organizations', - title: 'Organizations (JSON Array)', + id: 'domains', + title: 'Domains (JSON Array)', type: 'code', - placeholder: - '[{"name": "Company A", "domain": "companya.com"}, {"name": "Company B", "domain": "companyb.com"}]', + placeholder: '["apollo.io", "stripe.com"]', condition: { field: 'operation', value: 'organization_bulk_enrich' }, required: true, }, @@ -589,6 +588,14 @@ export const ApolloBlock: BlockConfig = { condition: { field: 'operation', value: 'account_bulk_update' }, mode: 'advanced', }, + { + id: 'account_bulk_update_account_stage_id', + title: 'Uniform Account Stage ID (used with Account IDs)', + type: 'short-input', + placeholder: 'Apollo account stage ID', + condition: { field: 'operation', value: 'account_bulk_update' }, + mode: 'advanced', + }, { id: 'account_attributes', title: 'Account Attributes (JSON Array of Objects)', @@ -1027,6 +1034,7 @@ Return ONLY the timestamp string in ISO 8601 format - no explanations, no quotes 'account_stage_ids', 'account_label_ids', 'people', + 'domains', 'organizations', 'contacts', 'accounts', @@ -1075,6 +1083,29 @@ Return ONLY the timestamp string in ISO 8601 format - no explanations, no quotes } } + if (params.operation === 'organization_bulk_enrich') { + // Back-compat: workflows saved before the `organizations` → `domains` rename stored an + // array of { name, domain? } objects (or plain strings) under `organizations`. Derive + // `domains` from it so those workflows keep running without manual migration. + if (parsedParams.domains === undefined && parsedParams.organizations !== undefined) { + const legacy = parsedParams.organizations + if (Array.isArray(legacy)) { + const derived = legacy + .map((item) => { + if (typeof item === 'string') return item + if (item && typeof item === 'object' && 'domain' in item) { + const domain = (item as Record).domain + return typeof domain === 'string' ? domain : undefined + } + return undefined + }) + .filter((domain): domain is string => typeof domain === 'string' && domain !== '') + if (derived.length > 0) parsedParams.domains = derived + } + } + parsedParams.organizations = undefined + } + if (params.operation === 'contact_bulk_update') { const { ids, attributes } = splitBulkUpdateInput(parsedParams.contacts) if (attributes) { @@ -1103,8 +1134,12 @@ Return ONLY the timestamp string in ISO 8601 format - no explanations, no quotes if (rest.account_bulk_update_owner_id) { parsedParams.owner_id = rest.account_bulk_update_owner_id } + if (rest.account_bulk_update_account_stage_id) { + parsedParams.account_stage_id = rest.account_bulk_update_account_stage_id + } parsedParams.account_bulk_update_name = undefined parsedParams.account_bulk_update_owner_id = undefined + parsedParams.account_bulk_update_account_stage_id = undefined } if (params.operation === 'contact_create') { @@ -1168,7 +1203,172 @@ Return ONLY the timestamp string in ISO 8601 format - no explanations, no quotes operation: { type: 'string', description: 'Apollo operation to perform' }, }, outputs: { - success: { type: 'boolean', description: 'Whether the operation was successful' }, - output: { type: 'json', description: 'Output data from the Apollo operation' }, + people: { + type: 'json', + description: + 'Array of people (people_search): [{id, first_name, last_name, name, title, email, organization_name, linkedin_url, phone_numbers}]', + }, + person: { + type: 'json', + description: + 'Enriched person (people_enrich): {id, first_name, last_name, name, title, email, organization_name, linkedin_url, phone_numbers}', + }, + matches: { + type: 'json', + description: 'Array of enriched people (people_bulk_enrich), null entries indicate no match', + }, + organizations: { + type: 'json', + description: + 'Array of organizations (organization_search, organization_bulk_enrich): [{id, name, website_url, linkedin_url, industry, phone, employees, founded_year}]', + }, + organization: { + type: 'json', + description: + 'Enriched organization (organization_enrich): {id, name, website_url, linkedin_url, industry, phone, employees, founded_year}', + }, + contact: { + type: 'json', + description: + 'Contact (contact_create, contact_update): {id, first_name, last_name, email, title, account_id, owner_id, created_at}', + }, + contacts: { + type: 'json', + description: 'Array of contacts (contact_search)', + }, + created_contacts: { + type: 'json', + description: 'Newly created contacts (contact_bulk_create)', + }, + existing_contacts: { + type: 'json', + description: 'Existing contacts (contact_bulk_create with dedupe)', + }, + account: { + type: 'json', + description: + 'Account (account_create, account_update): {id, name, domain, website_url, phone, owner_id, account_stage_id, created_at}', + }, + accounts: { + type: 'json', + description: 'Array of accounts (account_search)', + }, + created_accounts: { + type: 'json', + description: 'Newly created accounts (account_bulk_create)', + }, + existing_accounts: { + type: 'json', + description: 'Existing accounts (account_bulk_create with dedupe)', + }, + failed_accounts: { + type: 'json', + description: 'Accounts that failed (account_bulk_create)', + }, + account_ids: { + type: 'json', + description: 'IDs of updated accounts (account_bulk_update)', + }, + entity_progress_job: { + type: 'json', + description: 'Async job descriptor (contact_bulk_update, account_bulk_update async path)', + }, + opportunity: { + type: 'json', + description: + 'Opportunity (opportunity_create, opportunity_update, opportunity_get): {id, name, account_id, amount, opportunity_stage_id, owner_id, closed_date, is_closed, is_won, currency, created_at}', + }, + opportunities: { + type: 'json', + description: 'Array of opportunities (opportunity_search)', + }, + sequences: { + type: 'json', + description: + 'Array of sequences (sequence_search): [{id, name, active, num_steps, num_contacts, created_at}]', + }, + added: { + type: 'json', + description: + 'Contacts added to sequence (sequence_add): [{id, first_name, last_name, email, status}]', + }, + skipped: { + type: 'json', + description: 'Contacts skipped by sequence add (sequence_add)', + }, + skipped_contact_ids: { + type: 'json', + description: 'Skipped contact IDs (sequence_add): array of IDs or {id: reason} map', + }, + emailer_campaign: { + type: 'json', + description: 'Emailer campaign details (sequence_add): {id, name}', + }, + sequence_id: { + type: 'string', + description: 'Sequence ID contacts were added to (sequence_add)', + }, + tasks: { + type: 'json', + description: + 'Array of tasks (task_create, task_search): [{id, user_id, contact_id, type, priority, status, due_at, note, created_at}]', + }, + email_accounts: { + type: 'json', + description: + 'Linked email accounts (email_accounts): [{id, email, type, active, default, linked_at}]', + }, + pagination: { + type: 'json', + description: 'Pagination info (contact_search, account_search, task_search)', + }, + page: { type: 'number', description: 'Current page (search operations)' }, + per_page: { type: 'number', description: 'Results per page (search operations)' }, + total_entries: { + type: 'number', + description: 'Total entries matching search (search operations)', + }, + total_added: { type: 'number', description: 'Contacts added (sequence_add)' }, + total_skipped: { type: 'number', description: 'Contacts skipped (sequence_add)' }, + total_submitted: { + type: 'number', + description: 'Total submitted (contact_bulk_create, account_bulk_create)', + }, + created: { + type: 'boolean', + description: 'Created flag for single-item create operations', + }, + updated: { type: 'boolean', description: 'Updated flag for single-item update operations' }, + found: { type: 'boolean', description: 'Found flag (opportunity_get)' }, + enriched: { + type: 'boolean', + description: 'Enriched flag (people_enrich, organization_enrich)', + }, + message: { type: 'string', description: 'Message (bulk_update operations)' }, + job_id: { type: 'string', description: 'Async job ID (bulk_update operations)' }, + total: { + type: 'number', + description: 'Total count (organization_bulk_enrich requested domains; email_accounts count)', + }, + total_requested_enrichments: { + type: 'number', + description: 'Total requested enrichments (people_bulk_enrich)', + }, + unique_enriched_records: { + type: 'number', + description: 'Unique enriched records (people_bulk_enrich)', + }, + unique_domains: { + type: 'number', + description: 'Unique domains processed (organization_bulk_enrich)', + }, + missing_records: { + type: 'number', + description: 'Missing records (people_bulk_enrich, organization_bulk_enrich)', + }, + credits_consumed: { + type: 'number', + description: 'Credits consumed (people_bulk_enrich)', + }, }, } diff --git a/apps/sim/blocks/blocks/wiza.ts b/apps/sim/blocks/blocks/wiza.ts index 938e9318449..91a1acdff89 100644 --- a/apps/sim/blocks/blocks/wiza.ts +++ b/apps/sim/blocks/blocks/wiza.ts @@ -54,6 +54,15 @@ export const WizaBlock: BlockConfig = { placeholder: '[{"v":"CEO","s":"i"},{"v":"Founder","s":"i"}]', condition: { field: 'operation', value: 'prospect_search' }, mode: 'advanced', + wandConfig: { + enabled: true, + prompt: `Generate a Wiza job_title filter: a JSON array of include/exclude objects. +Each object is {"v": "", "s": "i" | "e"} where "i" includes and "e" excludes the title. Use double quotes around a phrase for an exact match. +Example: [{"v":"CEO","s":"i"},{"v":"intern","s":"e"}] +Return ONLY the JSON array - no explanations, no extra text.`, + placeholder: 'Describe the job titles to include/exclude...', + generationType: 'json-object', + }, }, { id: 'job_title_level', @@ -102,6 +111,15 @@ export const WizaBlock: BlockConfig = { placeholder: '[{"v":{"country":"united states"},"b":"city","s":"i"}]', condition: { field: 'operation', value: 'prospect_search' }, mode: 'advanced', + wandConfig: { + enabled: true, + prompt: `Generate a Wiza person location filter: a JSON array of location objects. +Each object is {"v": {"country": "...", "state": "...", "city": "..."}, "b": "country" | "state" | "city", "s": "i" | "e"} where "b" is the level to match, "s" includes ("i") or excludes ("e"). +Example: [{"v":{"country":"united states","state":"california"},"b":"state","s":"i"}] +Return ONLY the JSON array - no explanations, no extra text.`, + placeholder: 'Describe the person locations to include/exclude...', + generationType: 'json-object', + }, }, { id: 'skill', @@ -142,6 +160,15 @@ export const WizaBlock: BlockConfig = { placeholder: '[{"v":"wiza","s":"i"}]', condition: { field: 'operation', value: 'prospect_search' }, mode: 'advanced', + wandConfig: { + enabled: true, + prompt: `Generate a Wiza current-company filter: a JSON array of include/exclude objects. +Each object is {"v": "", "s": "i" | "e"} where "i" includes and "e" excludes the company. +Example: [{"v":"wiza","s":"i"},{"v":"acme","s":"e"}] +Return ONLY the JSON array - no explanations, no extra text.`, + placeholder: 'Describe the current companies to include/exclude...', + generationType: 'json-object', + }, }, { id: 'past_company', @@ -150,6 +177,15 @@ export const WizaBlock: BlockConfig = { placeholder: '[{"v":"google","s":"i"}]', condition: { field: 'operation', value: 'prospect_search' }, mode: 'advanced', + wandConfig: { + enabled: true, + prompt: `Generate a Wiza past-company filter: a JSON array of include/exclude objects. +Each object is {"v": "", "s": "i" | "e"} where "i" includes and "e" excludes the company. +Example: [{"v":"google","s":"i"}] +Return ONLY the JSON array - no explanations, no extra text.`, + placeholder: 'Describe the past companies to include/exclude...', + generationType: 'json-object', + }, }, { id: 'company_location', @@ -158,6 +194,15 @@ export const WizaBlock: BlockConfig = { placeholder: '[{"v":{"country":"canada"},"b":"country","s":"i"}]', condition: { field: 'operation', value: 'prospect_search' }, mode: 'advanced', + wandConfig: { + enabled: true, + prompt: `Generate a Wiza company HQ location filter: a JSON array of location objects. +Each object is {"v": {"country": "...", "state": "...", "city": "..."}, "b": "country" | "state" | "city", "s": "i" | "e"} where "b" is the level to match, "s" includes ("i") or excludes ("e"). +Example: [{"v":{"country":"canada","state":"ontario"},"b":"state","s":"i"}] +Return ONLY the JSON array - no explanations, no extra text.`, + placeholder: 'Describe the company HQ locations to include/exclude...', + generationType: 'json-object', + }, }, { id: 'company_industry', @@ -166,6 +211,15 @@ export const WizaBlock: BlockConfig = { placeholder: '[{"v":"computer software","s":"i"}]', condition: { field: 'operation', value: 'prospect_search' }, mode: 'advanced', + wandConfig: { + enabled: true, + prompt: `Generate a Wiza company industry filter: a JSON array of include/exclude objects. +Each object is {"v": "", "s": "i" | "e"} where "i" includes and "e" excludes the industry. +Example: [{"v":"computer software","s":"i"},{"v":"retail","s":"e"}] +Return ONLY the JSON array - no explanations, no extra text.`, + placeholder: 'Describe the company industries to include/exclude...', + generationType: 'json-object', + }, }, { id: 'company_size', @@ -190,6 +244,15 @@ export const WizaBlock: BlockConfig = { placeholder: '{"job_title":[{"v":"CEO","s":"i"}], "company_size":["11-50"]}', condition: { field: 'operation', value: 'prospect_search' }, mode: 'advanced', + wandConfig: { + enabled: true, + prompt: `Generate a Wiza prospect-search filters object as JSON. +Available keys: first_name, last_name (string arrays); job_title, job_company, past_company, company_industry (arrays of {"v":"...","s":"i"|"e"}); location, company_location (arrays of {"v":{country,state,city},"b":"country"|"state"|"city","s":"i"|"e"}); job_title_level, job_role, job_sub_role, skill, school, major, linkedin_slug, company_size, company_type (string arrays). +Example: {"job_title":[{"v":"CEO","s":"i"}],"company_size":["11-50","51-200"],"company_location":[{"v":{"country":"united states"},"b":"country","s":"i"}]} +Return ONLY the JSON object - no explanations, no extra text.`, + placeholder: 'Describe the prospects to search for...', + generationType: 'json-object', + }, }, // Company Enrichment @@ -235,7 +298,7 @@ export const WizaBlock: BlockConfig = { { label: 'Phone', id: 'phone' }, { label: 'Full', id: 'full' }, ], - value: () => 'full', + value: () => 'partial', condition: { field: 'operation', value: 'start_individual_reveal' }, required: { field: 'operation', value: 'start_individual_reveal' }, }, @@ -421,7 +484,159 @@ export const WizaBlock: BlockConfig = { }, outputs: { - success: { type: 'boolean', description: 'Whether the operation was successful' }, - output: { type: 'json', description: 'Output data from the Wiza operation' }, + total: { + type: 'number', + description: 'Total prospects matching filters (prospect_search)', + }, + profiles: { + type: 'json', + description: + 'Sample prospect profiles (prospect_search): [{full_name, linkedin_url, industry, job_title, job_title_role, job_title_sub_role, job_company_name, job_company_website, location_name}]', + }, + id: { + type: 'number', + description: 'Reveal ID (start_individual_reveal, get_individual_reveal)', + }, + status: { + type: 'string', + description: + 'Reveal status (start_individual_reveal, get_individual_reveal): queued | resolving | finished | failed', + }, + is_complete: { + type: 'boolean', + description: + 'Whether the reveal has completed (start_individual_reveal, get_individual_reveal)', + }, + name: { type: 'string', description: 'Full name (get_individual_reveal)' }, + company: { type: 'string', description: 'Company name (get_individual_reveal)' }, + enrichment_level: { + type: 'string', + description: 'Enrichment level used (get_individual_reveal)', + }, + linkedin_profile_url: { type: 'string', description: 'LinkedIn URL (get_individual_reveal)' }, + title: { type: 'string', description: 'Job title (get_individual_reveal)' }, + location: { type: 'string', description: 'Location (get_individual_reveal)' }, + email: { type: 'string', description: 'Primary email (get_individual_reveal)' }, + email_type: { type: 'string', description: 'Primary email type (get_individual_reveal)' }, + email_status: { + type: 'string', + description: 'Primary email status: valid | risky | unfound (get_individual_reveal)', + }, + emails: { + type: 'json', + description: 'All emails found (get_individual_reveal): [{email, email_type, email_status}]', + }, + mobile_phone: { type: 'string', description: 'Mobile phone (get_individual_reveal)' }, + phone_number: { type: 'string', description: 'Direct/office phone (get_individual_reveal)' }, + phone_status: { + type: 'string', + description: 'Phone status: found | unfound (get_individual_reveal)', + }, + phones: { + type: 'json', + description: 'All phones found (get_individual_reveal): [{number, pretty_number, type}]', + }, + company_name: { + type: 'string', + description: 'Company name (company_enrichment)', + }, + company_domain: { + type: 'string', + description: 'Company domain (company_enrichment, get_individual_reveal)', + }, + domain: { type: 'string', description: 'Domain (company_enrichment)' }, + company_industry: { + type: 'string', + description: 'Industry (company_enrichment, get_individual_reveal)', + }, + company_size: { + type: 'number', + description: 'Employee count (company_enrichment, get_individual_reveal)', + }, + company_size_range: { + type: 'string', + description: 'Headcount range (company_enrichment, get_individual_reveal)', + }, + company_founded: { + type: 'number', + description: 'Year founded (company_enrichment, get_individual_reveal)', + }, + company_revenue_range: { + type: 'string', + description: 'Revenue range (company_enrichment)', + }, + company_revenue: { type: 'string', description: 'Revenue (get_individual_reveal)' }, + company_funding: { + type: 'string', + description: 'Total funding (company_enrichment, get_individual_reveal)', + }, + company_type: { + type: 'string', + description: 'Company type (company_enrichment, get_individual_reveal)', + }, + company_description: { + type: 'string', + description: 'Company description (company_enrichment, get_individual_reveal)', + }, + company_ticker: { type: 'string', description: 'Stock ticker (company_enrichment)' }, + company_last_funding_round: { + type: 'string', + description: 'Last funding round (company_enrichment)', + }, + company_last_funding_amount: { + type: 'string', + description: 'Last funding amount (company_enrichment)', + }, + company_last_funding_at: { + type: 'string', + description: 'Last funding date (company_enrichment)', + }, + company_location: { + type: 'string', + description: 'Full location string (company_enrichment, get_individual_reveal)', + }, + company_twitter: { type: 'string', description: 'Twitter URL (company_enrichment)' }, + company_facebook: { type: 'string', description: 'Facebook URL (company_enrichment)' }, + company_linkedin: { + type: 'string', + description: 'LinkedIn URL (company_enrichment, get_individual_reveal)', + }, + company_linkedin_id: { type: 'string', description: 'LinkedIn ID (company_enrichment)' }, + company_street: { + type: 'string', + description: 'Street address (company_enrichment, get_individual_reveal)', + }, + company_locality: { + type: 'string', + description: 'City (company_enrichment, get_individual_reveal)', + }, + company_region: { + type: 'string', + description: 'State/region (company_enrichment, get_individual_reveal)', + }, + company_postal_code: { + type: 'string', + description: 'Postal code (company_enrichment, get_individual_reveal)', + }, + company_country: { + type: 'string', + description: 'Country (company_enrichment, get_individual_reveal)', + }, + company_subindustry: { type: 'string', description: 'Subindustry (get_individual_reveal)' }, + credits: { + type: 'json', + description: + 'Credits deducted — company_enrichment: { api_credits: { total, company_credits } }; get_individual_reveal: { api_credits: { total, email_credits, phone_credits, scrape_credits } }', + }, + email_credits: { + type: 'json', + description: 'Remaining email credits — number or "unlimited" (get_credits)', + }, + phone_credits: { + type: 'json', + description: 'Remaining phone credits — number or "unlimited" (get_credits)', + }, + export_credits: { type: 'number', description: 'Remaining export credits (get_credits)' }, + api_credits: { type: 'number', description: 'Remaining API credits (get_credits)' }, }, } diff --git a/apps/sim/blocks/blocks/zoominfo.ts b/apps/sim/blocks/blocks/zoominfo.ts new file mode 100644 index 00000000000..8160cc929f5 --- /dev/null +++ b/apps/sim/blocks/blocks/zoominfo.ts @@ -0,0 +1,554 @@ +import { ZoomInfoIcon } from '@/components/icons' +import type { BlockConfig } from '@/blocks/types' +import { AuthMode, IntegrationType } from '@/blocks/types' +import type { ZoomInfoResponse } from '@/tools/zoominfo/types' + +export const ZoomInfoBlock: BlockConfig = { + type: 'zoominfo', + name: 'ZoomInfo', + description: 'Search and enrich B2B company and contact data with ZoomInfo.', + authMode: AuthMode.ApiKey, + longDescription: + 'Integrates ZoomInfo into the workflow. Search companies and contacts, enrich firmographic and contact data, find intent signals, and pull news — all using the ZoomInfo GTM API.', + docsLink: 'https://docs.sim.ai/tools/zoominfo', + category: 'tools', + integrationType: IntegrationType.Sales, + tags: ['enrichment', 'sales-engagement'], + bgColor: '#EA1B15', + icon: ZoomInfoIcon, + subBlocks: [ + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + options: [ + { label: 'Search Companies', id: 'search_companies' }, + { label: 'Search Contacts', id: 'search_contacts' }, + { label: 'Enrich Companies', id: 'enrich_companies' }, + { label: 'Enrich Contacts', id: 'enrich_contacts' }, + { label: 'Search Intent', id: 'search_intent' }, + { label: 'Search News', id: 'search_news' }, + ], + value: () => 'search_companies', + }, + { + id: 'clientId', + title: 'ZoomInfo Client ID', + type: 'short-input', + placeholder: 'Enter your ZoomInfo OAuth client ID', + required: true, + }, + { + id: 'clientSecret', + title: 'ZoomInfo Client Secret', + type: 'short-input', + placeholder: 'Enter your ZoomInfo OAuth client secret', + password: true, + required: true, + }, + + // Search Companies + { + id: 'companyName', + title: 'Company Name', + type: 'short-input', + placeholder: 'Acme Corp', + condition: { field: 'operation', value: ['search_companies', 'search_contacts'] }, + }, + { + id: 'companyWebsite', + title: 'Company Website', + type: 'short-input', + placeholder: 'acme.com (comma-separated for multiple)', + condition: { field: 'operation', value: 'search_companies' }, + }, + { + id: 'companyTicker', + title: 'Ticker Symbol', + type: 'short-input', + placeholder: 'ACME', + condition: { field: 'operation', value: 'search_companies' }, + mode: 'advanced', + }, + { + id: 'industryCodes', + title: 'Industry Codes', + type: 'code', + placeholder: '["software","saas"]', + condition: { field: 'operation', value: ['search_companies', 'search_intent'] }, + mode: 'advanced', + }, + { + id: 'country', + title: 'Country', + type: 'short-input', + placeholder: 'United States', + condition: { field: 'operation', value: ['search_companies', 'search_intent'] }, + mode: 'advanced', + }, + { + id: 'state', + title: 'State / Province', + type: 'short-input', + placeholder: 'California', + condition: { field: 'operation', value: ['search_companies', 'search_intent'] }, + mode: 'advanced', + }, + { + id: 'metroRegion', + title: 'Metro Region', + type: 'short-input', + placeholder: 'San Francisco Bay Area', + condition: { field: 'operation', value: 'search_companies' }, + mode: 'advanced', + }, + { + id: 'revenueMin', + title: 'Min Revenue (thousands USD)', + type: 'short-input', + placeholder: '1000', + condition: { field: 'operation', value: 'search_companies' }, + mode: 'advanced', + }, + { + id: 'revenueMax', + title: 'Max Revenue (thousands USD)', + type: 'short-input', + placeholder: '100000', + condition: { field: 'operation', value: 'search_companies' }, + mode: 'advanced', + }, + { + id: 'employeeRangeMin', + title: 'Min Employees', + type: 'short-input', + placeholder: '10', + condition: { field: 'operation', value: 'search_companies' }, + mode: 'advanced', + }, + { + id: 'employeeRangeMax', + title: 'Max Employees', + type: 'short-input', + placeholder: '5000', + condition: { field: 'operation', value: 'search_companies' }, + mode: 'advanced', + }, + { + id: 'excludeDefunctCompanies', + title: 'Exclude Defunct Companies', + type: 'switch', + condition: { field: 'operation', value: 'search_companies' }, + mode: 'advanced', + }, + + // Search Contacts + { + id: 'firstName', + title: 'First Name', + type: 'short-input', + placeholder: 'Jane', + condition: { field: 'operation', value: 'search_contacts' }, + }, + { + id: 'lastName', + title: 'Last Name', + type: 'short-input', + placeholder: 'Doe', + condition: { field: 'operation', value: 'search_contacts' }, + }, + { + id: 'fullName', + title: 'Full Name', + type: 'short-input', + placeholder: 'Jane Doe', + condition: { field: 'operation', value: 'search_contacts' }, + mode: 'advanced', + }, + { + id: 'emailAddress', + title: 'Email Address', + type: 'short-input', + placeholder: 'jane@acme.com', + condition: { field: 'operation', value: 'search_contacts' }, + }, + { + id: 'jobTitle', + title: 'Job Title', + type: 'short-input', + placeholder: 'VP of Marketing', + condition: { field: 'operation', value: 'search_contacts' }, + }, + { + id: 'managementLevel', + title: 'Management Level', + type: 'code', + placeholder: '["vp","director","manager"]', + condition: { field: 'operation', value: 'search_contacts' }, + mode: 'advanced', + }, + { + id: 'department', + title: 'Department', + type: 'code', + placeholder: '["sales","marketing"]', + condition: { field: 'operation', value: 'search_contacts' }, + mode: 'advanced', + }, + { + id: 'companyId', + title: 'Company ID', + type: 'short-input', + placeholder: 'ZoomInfo company ID', + condition: { field: 'operation', value: 'search_contacts' }, + mode: 'advanced', + }, + { + id: 'contactAccuracyScoreMin', + title: 'Min Accuracy Score', + type: 'short-input', + placeholder: '70-99', + condition: { field: 'operation', value: 'search_contacts' }, + mode: 'advanced', + }, + { + id: 'requiredFields', + title: 'Required Fields', + type: 'code', + placeholder: '["email","phone"]', + condition: { field: 'operation', value: ['search_contacts', 'enrich_contacts'] }, + mode: 'advanced', + }, + { + id: 'excludePartialProfiles', + title: 'Exclude Partial Profiles', + type: 'switch', + condition: { field: 'operation', value: 'search_contacts' }, + mode: 'advanced', + }, + + // Enrich Companies / Contacts + { + id: 'matchCompanyInput', + title: 'Companies to Enrich (JSON Array)', + type: 'code', + placeholder: '[{"companyName":"Acme","companyWebsite":"acme.com"}]', + condition: { field: 'operation', value: 'enrich_companies' }, + required: { field: 'operation', value: 'enrich_companies' }, + }, + { + id: 'matchPersonInput', + title: 'Contacts to Enrich (JSON Array)', + type: 'code', + placeholder: '[{"firstName":"Jane","lastName":"Doe","companyName":"Acme"}]', + condition: { field: 'operation', value: 'enrich_contacts' }, + required: { field: 'operation', value: 'enrich_contacts' }, + }, + { + id: 'outputFields', + title: 'Output Fields', + type: 'code', + placeholder: '["id","name","website","revenue","employeeCount"]', + condition: { field: 'operation', value: ['enrich_companies', 'enrich_contacts'] }, + mode: 'advanced', + }, + + // Search Intent + { + id: 'topics', + title: 'Intent Topics', + type: 'code', + placeholder: '["CRM Software","Marketing Automation"]', + condition: { field: 'operation', value: 'search_intent' }, + required: { field: 'operation', value: 'search_intent' }, + }, + { + id: 'signalStartDate', + title: 'Signal Start Date', + type: 'short-input', + placeholder: 'YYYY-MM-DD', + condition: { field: 'operation', value: 'search_intent' }, + mode: 'advanced', + wandConfig: { + enabled: true, + prompt: + 'Generate a date in YYYY-MM-DD format. Return ONLY the date string, no quotes or explanation.', + placeholder: 'Describe the date (e.g., "30 days ago")...', + generationType: 'timestamp', + }, + }, + { + id: 'signalEndDate', + title: 'Signal End Date', + type: 'short-input', + placeholder: 'YYYY-MM-DD', + condition: { field: 'operation', value: 'search_intent' }, + mode: 'advanced', + wandConfig: { + enabled: true, + prompt: + 'Generate a date in YYYY-MM-DD format. Return ONLY the date string, no quotes or explanation.', + placeholder: 'Describe the date (e.g., "today")...', + generationType: 'timestamp', + }, + }, + { + id: 'signalScoreMin', + title: 'Min Signal Score (60-100)', + type: 'short-input', + placeholder: '60', + condition: { field: 'operation', value: 'search_intent' }, + mode: 'advanced', + }, + { + id: 'signalScoreMax', + title: 'Max Signal Score (60-100)', + type: 'short-input', + placeholder: '100', + condition: { field: 'operation', value: 'search_intent' }, + mode: 'advanced', + }, + { + id: 'audienceStrengthMin', + title: 'Min Audience Strength (A-E)', + type: 'dropdown', + options: [ + { label: 'A (largest)', id: 'A' }, + { label: 'B', id: 'B' }, + { label: 'C', id: 'C' }, + { label: 'D', id: 'D' }, + { label: 'E', id: 'E' }, + ], + condition: { field: 'operation', value: 'search_intent' }, + mode: 'advanced', + }, + { + id: 'audienceStrengthMax', + title: 'Max Audience Strength (A-E)', + type: 'dropdown', + options: [ + { label: 'A (largest)', id: 'A' }, + { label: 'B', id: 'B' }, + { label: 'C', id: 'C' }, + { label: 'D', id: 'D' }, + { label: 'E', id: 'E' }, + ], + condition: { field: 'operation', value: 'search_intent' }, + mode: 'advanced', + }, + { + id: 'findRecommendedContacts', + title: 'Include Recommended Contacts', + type: 'switch', + condition: { field: 'operation', value: 'search_intent' }, + mode: 'advanced', + }, + + // Search News + { + id: 'categories', + title: 'Categories', + type: 'code', + placeholder: '["funding","acquisition"]', + condition: { field: 'operation', value: 'search_news' }, + }, + { + id: 'url', + title: 'Source URLs', + type: 'code', + placeholder: '["https://techcrunch.com"]', + condition: { field: 'operation', value: 'search_news' }, + mode: 'advanced', + }, + { + id: 'pageDateMin', + title: 'Earliest Publish Date', + type: 'short-input', + placeholder: 'YYYY-MM-DD', + condition: { field: 'operation', value: 'search_news' }, + mode: 'advanced', + wandConfig: { + enabled: true, + prompt: + 'Generate a date in YYYY-MM-DD format. Return ONLY the date string, no quotes or explanation.', + placeholder: 'Describe the date (e.g., "last week")...', + generationType: 'timestamp', + }, + }, + { + id: 'pageDateMax', + title: 'Latest Publish Date', + type: 'short-input', + placeholder: 'YYYY-MM-DD', + condition: { field: 'operation', value: 'search_news' }, + mode: 'advanced', + wandConfig: { + enabled: true, + prompt: + 'Generate a date in YYYY-MM-DD format. Return ONLY the date string, no quotes or explanation.', + placeholder: 'Describe the date (e.g., "today")...', + generationType: 'timestamp', + }, + }, + + // Sort + Pagination + { + id: 'sortBy', + title: 'Sort By Field', + type: 'short-input', + placeholder: 'Field name', + condition: { field: 'operation', value: ['search_companies', 'search_contacts'] }, + mode: 'advanced', + }, + { + id: 'sortOrder', + title: 'Sort Order', + type: 'dropdown', + options: [ + { label: 'Ascending', id: 'asc' }, + { label: 'Descending', id: 'desc' }, + ], + condition: { field: 'operation', value: ['search_companies', 'search_contacts'] }, + mode: 'advanced', + }, + { + id: 'page', + title: 'Page Number', + type: 'short-input', + placeholder: '1', + condition: { + field: 'operation', + value: ['search_companies', 'search_contacts', 'search_intent', 'search_news'], + }, + mode: 'advanced', + }, + { + id: 'rpp', + title: 'Results Per Page', + type: 'short-input', + placeholder: '25 (max 100)', + condition: { + field: 'operation', + value: ['search_companies', 'search_contacts', 'search_intent', 'search_news'], + }, + mode: 'advanced', + }, + ], + tools: { + access: [ + 'zoominfo_search_companies', + 'zoominfo_search_contacts', + 'zoominfo_enrich_companies', + 'zoominfo_enrich_contacts', + 'zoominfo_search_intent', + 'zoominfo_search_news', + ], + config: { + tool: (params) => { + switch (params.operation) { + case 'search_companies': + return 'zoominfo_search_companies' + case 'search_contacts': + return 'zoominfo_search_contacts' + case 'enrich_companies': + return 'zoominfo_enrich_companies' + case 'enrich_contacts': + return 'zoominfo_enrich_contacts' + case 'search_intent': + return 'zoominfo_search_intent' + case 'search_news': + return 'zoominfo_search_news' + default: + throw new Error(`Invalid ZoomInfo operation: ${params.operation}`) + } + }, + params: (params) => { + const { operation: _operation, ...rest } = params as Record + const parsed: Record = { ...rest } + + const toNumber = (key: string) => { + const v = parsed[key] + if (v === undefined || v === null || v === '') return + const n = Number(v) + if (Number.isFinite(n)) parsed[key] = n + } + + for (const key of [ + 'revenueMin', + 'revenueMax', + 'employeeRangeMin', + 'employeeRangeMax', + 'contactAccuracyScoreMin', + 'signalScoreMin', + 'signalScoreMax', + 'page', + 'rpp', + ]) { + toNumber(key) + } + + return parsed + }, + }, + }, + inputs: { + operation: { type: 'string', description: 'ZoomInfo operation to perform' }, + clientId: { type: 'string', description: 'ZoomInfo OAuth client ID' }, + clientSecret: { type: 'string', description: 'ZoomInfo OAuth client secret' }, + companyName: { type: 'string', description: 'Company name' }, + companyWebsite: { type: 'string', description: 'Company website' }, + companyTicker: { type: 'string', description: 'Stock ticker' }, + industryCodes: { type: 'string', description: 'Industry codes' }, + country: { type: 'string', description: 'Country' }, + state: { type: 'string', description: 'State' }, + metroRegion: { type: 'string', description: 'Metro region' }, + revenueMin: { type: 'number', description: 'Min revenue (thousands USD)' }, + revenueMax: { type: 'number', description: 'Max revenue (thousands USD)' }, + employeeRangeMin: { type: 'number', description: 'Min employees' }, + employeeRangeMax: { type: 'number', description: 'Max employees' }, + excludeDefunctCompanies: { type: 'boolean', description: 'Exclude defunct companies' }, + firstName: { type: 'string', description: 'First name' }, + lastName: { type: 'string', description: 'Last name' }, + fullName: { type: 'string', description: 'Full name' }, + emailAddress: { type: 'string', description: 'Email address' }, + jobTitle: { type: 'string', description: 'Job title' }, + managementLevel: { type: 'string', description: 'Management level' }, + department: { type: 'string', description: 'Department' }, + companyId: { type: 'string', description: 'Company ID' }, + contactAccuracyScoreMin: { type: 'number', description: 'Min accuracy score' }, + requiredFields: { type: 'string', description: 'Required fields' }, + excludePartialProfiles: { type: 'boolean', description: 'Exclude partial profiles' }, + matchCompanyInput: { type: 'string', description: 'Companies to enrich (JSON array)' }, + matchPersonInput: { type: 'string', description: 'Contacts to enrich (JSON array)' }, + outputFields: { type: 'string', description: 'Output fields' }, + topics: { type: 'string', description: 'Intent topics' }, + signalStartDate: { type: 'string', description: 'Signal start date' }, + signalEndDate: { type: 'string', description: 'Signal end date' }, + signalScoreMin: { type: 'number', description: 'Min signal score' }, + signalScoreMax: { type: 'number', description: 'Max signal score' }, + audienceStrengthMin: { type: 'string', description: 'Min audience strength (A-E)' }, + audienceStrengthMax: { type: 'string', description: 'Max audience strength (A-E)' }, + findRecommendedContacts: { type: 'boolean', description: 'Include recommended contacts' }, + categories: { type: 'string', description: 'News categories' }, + url: { type: 'string', description: 'News source URLs' }, + pageDateMin: { type: 'string', description: 'Earliest publish date' }, + pageDateMax: { type: 'string', description: 'Latest publish date' }, + sortBy: { type: 'string', description: 'Sort field' }, + sortOrder: { type: 'string', description: 'Sort order' }, + page: { type: 'number', description: 'Page number' }, + rpp: { type: 'number', description: 'Results per page' }, + }, + outputs: { + companies: { type: 'json', description: 'Matching companies (search_companies)' }, + contacts: { type: 'json', description: 'Matching contacts (search_contacts)' }, + results: { + type: 'json', + description: 'Enrichment results (enrich_companies / enrich_contacts)', + }, + signals: { type: 'json', description: 'Intent signals (search_intent)' }, + articles: { type: 'json', description: 'News articles (search_news)' }, + totalResults: { type: 'number', description: 'Total matching results across all pages' }, + currentPage: { type: 'number', description: 'Current page number' }, + totalPages: { type: 'number', description: 'Total number of pages available' }, + }, +} diff --git a/apps/sim/blocks/registry.ts b/apps/sim/blocks/registry.ts index 79e8191546f..7e8659ef67c 100644 --- a/apps/sim/blocks/registry.ts +++ b/apps/sim/blocks/registry.ts @@ -243,6 +243,7 @@ import { YouTubeBlock } from '@/blocks/blocks/youtube' import { ZendeskBlock } from '@/blocks/blocks/zendesk' import { ZepBlock } from '@/blocks/blocks/zep' import { ZoomBlock } from '@/blocks/blocks/zoom' +import { ZoomInfoBlock } from '@/blocks/blocks/zoominfo' import type { BlockConfig } from '@/blocks/types' // Registry of all available blocks, alphabetically sorted @@ -513,6 +514,7 @@ export const registry: Record = { zendesk: ZendeskBlock, zep: ZepBlock, zoom: ZoomBlock, + zoominfo: ZoomInfoBlock, } export const getBlock = (type: string): BlockConfig | undefined => { diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index a35727905dd..2240f766529 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -4683,6 +4683,36 @@ export function ZoomIcon(props: SVGProps) { ) } +export function ZoomInfoIcon(props: SVGProps) { + const id = useId() + const clipId = `zoominfo-clip_${id}` + return ( + + ) +} + export function SendgridIcon(props: SVGProps) { return ( 0) { @@ -83,7 +90,8 @@ export const apolloAccountBulkUpdateTool: ToolConfig< body.account_attributes = params.account_attributes } } - const hasUpdateFields = body.account_attributes || body.name || body.owner_id + const hasUpdateFields = + body.account_attributes || body.name || body.owner_id || body.account_stage_id if (!hasUpdateFields) { throw new Error( 'Apollo account bulk update requires update fields. Provide account_attributes (array of per-account updates with id, or single object paired with account_ids), or pair account_ids with name/owner_id to apply uniformly.' @@ -116,25 +124,53 @@ export const apolloAccountBulkUpdateTool: ToolConfig< } const data = await response.json() + const accounts = Array.isArray(data?.accounts) ? data.accounts : [] + const entityProgressJob = data?.entity_progress_job ?? null + const accountIds = Array.isArray(data?.account_ids) + ? data.account_ids + : accounts.map((a: { id?: unknown }) => a?.id).filter((id: unknown) => typeof id === 'string') + const jobId = + typeof data?.job_id === 'string' + ? data.job_id + : typeof entityProgressJob?.id === 'string' + ? entityProgressJob.id + : null return { success: true, output: { - message: data.message ?? null, - account_ids: data.account_ids ?? [], + accounts, + account_ids: accountIds, + entity_progress_job: entityProgressJob, + job_id: jobId, + message: data?.message ?? null, }, } }, outputs: { - message: { - type: 'string', - description: 'Confirmation message from Apollo', - optional: true, + accounts: { + type: 'json', + description: 'Updated accounts (synchronous response): [{id, account_stage_id, ...}]', }, account_ids: { type: 'json', description: 'IDs of accounts that were updated', }, + entity_progress_job: { + type: 'json', + description: 'Async job descriptor (when async=true is passed with account_ids)', + optional: true, + }, + job_id: { + type: 'string', + description: 'Async job ID extracted from entity_progress_job', + optional: true, + }, + message: { + type: 'string', + description: 'Optional confirmation message from Apollo', + optional: true, + }, }, } diff --git a/apps/sim/tools/apollo/account_create.ts b/apps/sim/tools/apollo/account_create.ts index 220dfccc606..c59a3f285ab 100644 --- a/apps/sim/tools/apollo/account_create.ts +++ b/apps/sim/tools/apollo/account_create.ts @@ -14,7 +14,7 @@ export const apolloAccountCreateTool: ToolConfig< apiKey: { type: 'string', required: true, - visibility: 'hidden', + visibility: 'user-only', description: 'Apollo API key (master key required)', }, name: { diff --git a/apps/sim/tools/apollo/account_search.ts b/apps/sim/tools/apollo/account_search.ts index e60686d9347..9eb50b4dab7 100644 --- a/apps/sim/tools/apollo/account_search.ts +++ b/apps/sim/tools/apollo/account_search.ts @@ -15,7 +15,7 @@ export const apolloAccountSearchTool: ToolConfig< apiKey: { type: 'string', required: true, - visibility: 'hidden', + visibility: 'user-only', description: 'Apollo API key (master key required)', }, q_organization_name: { diff --git a/apps/sim/tools/apollo/account_update.ts b/apps/sim/tools/apollo/account_update.ts index acaef42cc60..a08f446fb66 100644 --- a/apps/sim/tools/apollo/account_update.ts +++ b/apps/sim/tools/apollo/account_update.ts @@ -14,7 +14,7 @@ export const apolloAccountUpdateTool: ToolConfig< apiKey: { type: 'string', required: true, - visibility: 'hidden', + visibility: 'user-only', description: 'Apollo API key', }, account_id: { diff --git a/apps/sim/tools/apollo/contact_bulk_create.ts b/apps/sim/tools/apollo/contact_bulk_create.ts index 5a012485741..fd9adf0579f 100644 --- a/apps/sim/tools/apollo/contact_bulk_create.ts +++ b/apps/sim/tools/apollo/contact_bulk_create.ts @@ -18,7 +18,7 @@ export const apolloContactBulkCreateTool: ToolConfig< apiKey: { type: 'string', required: true, - visibility: 'hidden', + visibility: 'user-only', description: 'Apollo API key (master key required)', }, contacts: { diff --git a/apps/sim/tools/apollo/contact_bulk_update.ts b/apps/sim/tools/apollo/contact_bulk_update.ts index 298873022b5..6157f7b5875 100644 --- a/apps/sim/tools/apollo/contact_bulk_update.ts +++ b/apps/sim/tools/apollo/contact_bulk_update.ts @@ -18,7 +18,7 @@ export const apolloContactBulkUpdateTool: ToolConfig< apiKey: { type: 'string', required: true, - visibility: 'hidden', + visibility: 'user-only', description: 'Apollo API key (master key required)', }, contact_ids: { @@ -95,25 +95,44 @@ export const apolloContactBulkUpdateTool: ToolConfig< } const data = await response.json() + const contacts = Array.isArray(data?.contacts) ? data.contacts : [] + const entityProgressJob = data?.entity_progress_job ?? null + const jobId = + typeof data?.job_id === 'string' + ? data.job_id + : typeof entityProgressJob?.id === 'string' + ? entityProgressJob.id + : null return { success: true, output: { - message: data.message ?? null, - job_id: data.job_id ?? null, + contacts, + entity_progress_job: entityProgressJob, + job_id: jobId, + message: data?.message ?? null, }, } }, outputs: { - message: { - type: 'string', - description: 'Confirmation message from Apollo', + contacts: { + type: 'json', + description: 'Updated contacts (synchronous response, ≤100 contacts)', + }, + entity_progress_job: { + type: 'json', + description: 'Async job descriptor (>100 contacts or async=true): {id, status, ...}', optional: true, }, job_id: { type: 'string', - description: 'Async job ID (returned for >100 contacts)', + description: 'Async job ID extracted from entity_progress_job', + optional: true, + }, + message: { + type: 'string', + description: 'Optional confirmation message from Apollo', optional: true, }, }, diff --git a/apps/sim/tools/apollo/contact_create.ts b/apps/sim/tools/apollo/contact_create.ts index c1ebb6e2b62..a71029a0b2d 100644 --- a/apps/sim/tools/apollo/contact_create.ts +++ b/apps/sim/tools/apollo/contact_create.ts @@ -14,7 +14,7 @@ export const apolloContactCreateTool: ToolConfig< apiKey: { type: 'string', required: true, - visibility: 'hidden', + visibility: 'user-only', description: 'Apollo API key', }, first_name: { diff --git a/apps/sim/tools/apollo/contact_search.ts b/apps/sim/tools/apollo/contact_search.ts index 7295b745c50..34413b5c206 100644 --- a/apps/sim/tools/apollo/contact_search.ts +++ b/apps/sim/tools/apollo/contact_search.ts @@ -14,7 +14,7 @@ export const apolloContactSearchTool: ToolConfig< apiKey: { type: 'string', required: true, - visibility: 'hidden', + visibility: 'user-only', description: 'Apollo API key', }, q_keywords: { diff --git a/apps/sim/tools/apollo/contact_update.ts b/apps/sim/tools/apollo/contact_update.ts index 3c99984ca6f..e9a297b1ae2 100644 --- a/apps/sim/tools/apollo/contact_update.ts +++ b/apps/sim/tools/apollo/contact_update.ts @@ -14,7 +14,7 @@ export const apolloContactUpdateTool: ToolConfig< apiKey: { type: 'string', required: true, - visibility: 'hidden', + visibility: 'user-only', description: 'Apollo API key', }, contact_id: { @@ -124,8 +124,7 @@ export const apolloContactUpdateTool: ToolConfig< type: 'json', required: false, visibility: 'user-or-llm', - description: - 'Custom field values keyed by custom field ID (accepted by Apollo but not officially documented for PATCH /contacts/{id})', + description: 'Custom field values keyed by custom field ID', }, }, diff --git a/apps/sim/tools/apollo/email_accounts.ts b/apps/sim/tools/apollo/email_accounts.ts index 96b9b58ee94..092aa079e5a 100644 --- a/apps/sim/tools/apollo/email_accounts.ts +++ b/apps/sim/tools/apollo/email_accounts.ts @@ -14,7 +14,7 @@ export const apolloEmailAccountsTool: ToolConfig< apiKey: { type: 'string', required: true, - visibility: 'hidden', + visibility: 'user-only', description: 'Apollo API key (master key required)', }, }, diff --git a/apps/sim/tools/apollo/opportunity_create.ts b/apps/sim/tools/apollo/opportunity_create.ts index 5c949fc692c..aefce10c841 100644 --- a/apps/sim/tools/apollo/opportunity_create.ts +++ b/apps/sim/tools/apollo/opportunity_create.ts @@ -17,7 +17,7 @@ export const apolloOpportunityCreateTool: ToolConfig< apiKey: { type: 'string', required: true, - visibility: 'hidden', + visibility: 'user-only', description: 'Apollo API key (master key required)', }, name: { diff --git a/apps/sim/tools/apollo/opportunity_get.ts b/apps/sim/tools/apollo/opportunity_get.ts index db8613b91eb..16217ea6712 100644 --- a/apps/sim/tools/apollo/opportunity_get.ts +++ b/apps/sim/tools/apollo/opportunity_get.ts @@ -14,7 +14,7 @@ export const apolloOpportunityGetTool: ToolConfig< apiKey: { type: 'string', required: true, - visibility: 'hidden', + visibility: 'user-only', description: 'Apollo API key', }, opportunity_id: { diff --git a/apps/sim/tools/apollo/opportunity_search.ts b/apps/sim/tools/apollo/opportunity_search.ts index c7fa7245292..1bcfb444645 100644 --- a/apps/sim/tools/apollo/opportunity_search.ts +++ b/apps/sim/tools/apollo/opportunity_search.ts @@ -17,7 +17,7 @@ export const apolloOpportunitySearchTool: ToolConfig< apiKey: { type: 'string', required: true, - visibility: 'hidden', + visibility: 'user-only', description: 'Apollo API key', }, sort_by_field: { diff --git a/apps/sim/tools/apollo/opportunity_update.ts b/apps/sim/tools/apollo/opportunity_update.ts index aea000414f8..ad553368733 100644 --- a/apps/sim/tools/apollo/opportunity_update.ts +++ b/apps/sim/tools/apollo/opportunity_update.ts @@ -17,7 +17,7 @@ export const apolloOpportunityUpdateTool: ToolConfig< apiKey: { type: 'string', required: true, - visibility: 'hidden', + visibility: 'user-only', description: 'Apollo API key', }, opportunity_id: { diff --git a/apps/sim/tools/apollo/organization_bulk_enrich.ts b/apps/sim/tools/apollo/organization_bulk_enrich.ts index d461318431c..923a1a31ee1 100644 --- a/apps/sim/tools/apollo/organization_bulk_enrich.ts +++ b/apps/sim/tools/apollo/organization_bulk_enrich.ts @@ -17,29 +17,32 @@ export const apolloOrganizationBulkEnrichTool: ToolConfig< apiKey: { type: 'string', required: true, - visibility: 'hidden', + visibility: 'user-only', description: 'Apollo API key', }, - organizations: { + domains: { type: 'array', required: true, visibility: 'user-or-llm', description: - 'Array of organizations to enrich (max 10). Each item requires `name` and may include `domain` (e.g., [{"name": "Example Corp", "domain": "example.com"}])', + 'Array of company domains to enrich (max 10, no www. or @, e.g., ["apollo.io", "stripe.com"])', }, }, request: { - url: 'https://api.apollo.io/api/v1/organizations/bulk_enrich', + url: (params: ApolloOrganizationBulkEnrichParams) => { + const qs = new URLSearchParams() + for (const domain of params.domains.slice(0, 10)) { + const trimmed = typeof domain === 'string' ? domain.trim() : '' + if (trimmed) qs.append('domains[]', trimmed) + } + return `https://api.apollo.io/api/v1/organizations/bulk_enrich?${qs.toString()}` + }, method: 'POST', headers: (params: ApolloOrganizationBulkEnrichParams) => ({ - 'Content-Type': 'application/json', 'Cache-Control': 'no-cache', 'X-Api-Key': params.apiKey, }), - body: (params: ApolloOrganizationBulkEnrichParams) => ({ - organizations: params.organizations.slice(0, 10), - }), }, transformResponse: async (response: Response) => { diff --git a/apps/sim/tools/apollo/organization_enrich.ts b/apps/sim/tools/apollo/organization_enrich.ts index 6c3702c56ab..376de2514eb 100644 --- a/apps/sim/tools/apollo/organization_enrich.ts +++ b/apps/sim/tools/apollo/organization_enrich.ts @@ -17,7 +17,7 @@ export const apolloOrganizationEnrichTool: ToolConfig< apiKey: { type: 'string', required: true, - visibility: 'hidden', + visibility: 'user-only', description: 'Apollo API key', }, domain: { @@ -29,20 +29,19 @@ export const apolloOrganizationEnrichTool: ToolConfig< }, request: { - url: 'https://api.apollo.io/api/v1/organizations/enrich', - method: 'POST', - headers: (params: ApolloOrganizationEnrichParams) => ({ - 'Content-Type': 'application/json', - 'Cache-Control': 'no-cache', - 'X-Api-Key': params.apiKey, - }), - body: (params: ApolloOrganizationEnrichParams) => { + url: (params: ApolloOrganizationEnrichParams) => { const domain = params.domain?.trim() if (!domain) { throw new Error('domain is required for organization enrichment') } - return { domain } + const qs = new URLSearchParams({ domain }) + return `https://api.apollo.io/api/v1/organizations/enrich?${qs.toString()}` }, + method: 'GET', + headers: (params: ApolloOrganizationEnrichParams) => ({ + 'Cache-Control': 'no-cache', + 'X-Api-Key': params.apiKey, + }), }, transformResponse: async (response: Response) => { diff --git a/apps/sim/tools/apollo/organization_search.ts b/apps/sim/tools/apollo/organization_search.ts index 1b6213039fb..433bfe2e368 100644 --- a/apps/sim/tools/apollo/organization_search.ts +++ b/apps/sim/tools/apollo/organization_search.ts @@ -17,7 +17,7 @@ export const apolloOrganizationSearchTool: ToolConfig< apiKey: { type: 'string', required: true, - visibility: 'hidden', + visibility: 'user-only', description: 'Apollo API key', }, organization_locations: { @@ -78,43 +78,32 @@ export const apolloOrganizationSearchTool: ToolConfig< }, request: { - url: 'https://api.apollo.io/api/v1/mixed_companies/search', + url: (params: ApolloOrganizationSearchParams) => { + const qs = new URLSearchParams() + qs.set('page', String(params.page || 1)) + qs.set('per_page', String(Math.min(params.per_page || 25, 100))) + + const appendArray = (key: string, values?: string[]) => { + if (!values?.length) return + for (const v of values) { + if (typeof v === 'string' && v) qs.append(`${key}[]`, v) + } + } + appendArray('organization_locations', params.organization_locations) + appendArray('organization_not_locations', params.organization_not_locations) + appendArray('organization_num_employees_ranges', params.organization_num_employees_ranges) + appendArray('q_organization_keyword_tags', params.q_organization_keyword_tags) + appendArray('organization_ids', params.organization_ids) + appendArray('q_organization_domains_list', params.q_organization_domains_list) + if (params.q_organization_name) qs.set('q_organization_name', params.q_organization_name) + + return `https://api.apollo.io/api/v1/mixed_companies/search?${qs.toString()}` + }, method: 'POST', headers: (params: ApolloOrganizationSearchParams) => ({ - 'Content-Type': 'application/json', 'Cache-Control': 'no-cache', 'X-Api-Key': params.apiKey, }), - body: (params: ApolloOrganizationSearchParams) => { - const body: Record = { - page: params.page || 1, - per_page: Math.min(params.per_page || 25, 100), - } - - if (params.organization_locations?.length) { - body.organization_locations = params.organization_locations - } - if (params.organization_not_locations?.length) { - body.organization_not_locations = params.organization_not_locations - } - if (params.organization_num_employees_ranges?.length) { - body.organization_num_employees_ranges = params.organization_num_employees_ranges - } - if (params.q_organization_keyword_tags?.length) { - body.q_organization_keyword_tags = params.q_organization_keyword_tags - } - if (params.q_organization_name) { - body.q_organization_name = params.q_organization_name - } - if (params.organization_ids?.length) { - body.organization_ids = params.organization_ids - } - if (params.q_organization_domains_list?.length) { - body.q_organization_domains_list = params.q_organization_domains_list - } - - return body - }, }, transformResponse: async (response: Response) => { diff --git a/apps/sim/tools/apollo/people_bulk_enrich.ts b/apps/sim/tools/apollo/people_bulk_enrich.ts index 9c9721ff9a8..f8eadb850dd 100644 --- a/apps/sim/tools/apollo/people_bulk_enrich.ts +++ b/apps/sim/tools/apollo/people_bulk_enrich.ts @@ -17,7 +17,7 @@ export const apolloPeopleBulkEnrichTool: ToolConfig< apiKey: { type: 'string', required: true, - visibility: 'hidden', + visibility: 'user-only', description: 'Apollo API key', }, people: { diff --git a/apps/sim/tools/apollo/people_enrich.ts b/apps/sim/tools/apollo/people_enrich.ts index c231bcf3c07..847b15e5489 100644 --- a/apps/sim/tools/apollo/people_enrich.ts +++ b/apps/sim/tools/apollo/people_enrich.ts @@ -14,7 +14,7 @@ export const apolloPeopleEnrichTool: ToolConfig< apiKey: { type: 'string', required: true, - visibility: 'hidden', + visibility: 'user-only', description: 'Apollo API key', }, first_name: { @@ -95,6 +95,15 @@ export const apolloPeopleEnrichTool: ToolConfig< request: { url: (params: ApolloPeopleEnrichParams) => { const qs = new URLSearchParams() + if (params.first_name) qs.set('first_name', params.first_name) + if (params.last_name) qs.set('last_name', params.last_name) + if (params.name) qs.set('name', params.name) + if (params.email) qs.set('email', params.email) + if (params.hashed_email) qs.set('hashed_email', params.hashed_email) + if (params.id) qs.set('id', params.id) + if (params.organization_name) qs.set('organization_name', params.organization_name) + if (params.domain) qs.set('domain', params.domain) + if (params.linkedin_url) qs.set('linkedin_url', params.linkedin_url) if (params.reveal_personal_emails !== undefined) { qs.set('reveal_personal_emails', String(params.reveal_personal_emails)) } @@ -109,25 +118,9 @@ export const apolloPeopleEnrichTool: ToolConfig< }, method: 'POST', headers: (params: ApolloPeopleEnrichParams) => ({ - 'Content-Type': 'application/json', 'Cache-Control': 'no-cache', 'X-Api-Key': params.apiKey, }), - body: (params: ApolloPeopleEnrichParams) => { - const body: Record = {} - - if (params.first_name) body.first_name = params.first_name - if (params.last_name) body.last_name = params.last_name - if (params.name) body.name = params.name - if (params.email) body.email = params.email - if (params.hashed_email) body.hashed_email = params.hashed_email - if (params.id) body.id = params.id - if (params.organization_name) body.organization_name = params.organization_name - if (params.domain) body.domain = params.domain - if (params.linkedin_url) body.linkedin_url = params.linkedin_url - - return body - }, }, transformResponse: async (response: Response) => { diff --git a/apps/sim/tools/apollo/people_search.ts b/apps/sim/tools/apollo/people_search.ts index 7e4bf528a32..08ebf4a98b0 100644 --- a/apps/sim/tools/apollo/people_search.ts +++ b/apps/sim/tools/apollo/people_search.ts @@ -14,7 +14,7 @@ export const apolloPeopleSearchTool: ToolConfig< apiKey: { type: 'string', required: true, - visibility: 'hidden', + visibility: 'user-only', description: 'Apollo API key', }, person_titles: { @@ -165,13 +165,17 @@ export const apolloPeopleSearchTool: ToolConfig< const data = await response.json() + // The legacy /mixed_people/search endpoint nests pagination under `pagination`, + // while api_search returns these top-level. Read both shapes defensively. + const pagination = data.pagination ?? {} + return { success: true, output: { people: data.people || [], - page: data.pagination?.page || 1, - per_page: data.pagination?.per_page || 25, - total_entries: data.pagination?.total_entries || 0, + page: pagination.page ?? data.page ?? 1, + per_page: pagination.per_page ?? data.per_page ?? 25, + total_entries: pagination.total_entries ?? data.total_entries ?? 0, }, } }, diff --git a/apps/sim/tools/apollo/sequence_add_contacts.ts b/apps/sim/tools/apollo/sequence_add_contacts.ts index 7008a5d894f..48d04daa51f 100644 --- a/apps/sim/tools/apollo/sequence_add_contacts.ts +++ b/apps/sim/tools/apollo/sequence_add_contacts.ts @@ -17,7 +17,7 @@ export const apolloSequenceAddContactsTool: ToolConfig< apiKey: { type: 'string', required: true, - visibility: 'hidden', + visibility: 'user-only', description: 'Apollo API key (master key required)', }, sequence_id: { @@ -128,15 +128,12 @@ export const apolloSequenceAddContactsTool: ToolConfig< }, request: { - url: (params: ApolloSequenceAddContactsParams) => - `https://api.apollo.io/api/v1/emailer_campaigns/${params.sequence_id.trim()}/add_contact_ids`, - method: 'POST', - headers: (params: ApolloSequenceAddContactsParams) => ({ - 'Content-Type': 'application/json', - 'Cache-Control': 'no-cache', - 'X-Api-Key': params.apiKey, - }), - body: (params: ApolloSequenceAddContactsParams) => { + /** + * Apollo documents every field for this endpoint as a query parameter (no request body), + * so `contact_ids[]`/`label_names[]` are appended to the query string alongside the scalar + * settings, matching the documented contract. + */ + url: (params: ApolloSequenceAddContactsParams) => { const hasContactIds = !!params.contact_ids?.length const hasLabelNames = !!params.label_names?.length if (!hasContactIds && !hasLabelNames) { @@ -144,43 +141,67 @@ export const apolloSequenceAddContactsTool: ToolConfig< 'Apollo sequence add requires either contact_ids or label_names to be provided' ) } - const body: Record = { - emailer_campaign_id: params.sequence_id, - send_email_from_email_account_id: params.send_email_from_email_account_id, + const qs = new URLSearchParams() + qs.set('emailer_campaign_id', params.sequence_id) + qs.set('send_email_from_email_account_id', params.send_email_from_email_account_id) + for (const id of params.contact_ids ?? []) { + if (typeof id === 'string' && id.length > 0) qs.append('contact_ids[]', id) + } + for (const name of params.label_names ?? []) { + if (typeof name === 'string' && name.length > 0) qs.append('label_names[]', name) } - if (hasContactIds) body.contact_ids = params.contact_ids - if (hasLabelNames) body.label_names = params.label_names if (params.send_email_from_email_address) { - body.send_email_from_email_address = params.send_email_from_email_address + qs.set('send_email_from_email_address', params.send_email_from_email_address) + } + if (params.sequence_no_email !== undefined) { + qs.set('sequence_no_email', String(params.sequence_no_email)) } - if (params.sequence_no_email !== undefined) body.sequence_no_email = params.sequence_no_email if (params.sequence_unverified_email !== undefined) { - body.sequence_unverified_email = params.sequence_unverified_email + qs.set('sequence_unverified_email', String(params.sequence_unverified_email)) } if (params.sequence_job_change !== undefined) { - body.sequence_job_change = params.sequence_job_change + qs.set('sequence_job_change', String(params.sequence_job_change)) } if (params.sequence_active_in_other_campaigns !== undefined) { - body.sequence_active_in_other_campaigns = params.sequence_active_in_other_campaigns + qs.set( + 'sequence_active_in_other_campaigns', + String(params.sequence_active_in_other_campaigns) + ) } if (params.sequence_finished_in_other_campaigns !== undefined) { - body.sequence_finished_in_other_campaigns = params.sequence_finished_in_other_campaigns + qs.set( + 'sequence_finished_in_other_campaigns', + String(params.sequence_finished_in_other_campaigns) + ) } if (params.sequence_same_company_in_same_campaign !== undefined) { - body.sequence_same_company_in_same_campaign = params.sequence_same_company_in_same_campaign + qs.set( + 'sequence_same_company_in_same_campaign', + String(params.sequence_same_company_in_same_campaign) + ) } if (params.contacts_without_ownership_permission !== undefined) { - body.contacts_without_ownership_permission = params.contacts_without_ownership_permission + qs.set( + 'contacts_without_ownership_permission', + String(params.contacts_without_ownership_permission) + ) + } + if (params.add_if_in_queue !== undefined) { + qs.set('add_if_in_queue', String(params.add_if_in_queue)) } - if (params.add_if_in_queue !== undefined) body.add_if_in_queue = params.add_if_in_queue if (params.contact_verification_skipped !== undefined) { - body.contact_verification_skipped = params.contact_verification_skipped + qs.set('contact_verification_skipped', String(params.contact_verification_skipped)) } - if (params.user_id) body.user_id = params.user_id - if (params.status) body.status = params.status - if (params.auto_unpause_at) body.auto_unpause_at = params.auto_unpause_at - return body + if (params.user_id) qs.set('user_id', params.user_id) + if (params.status) qs.set('status', params.status) + if (params.auto_unpause_at) qs.set('auto_unpause_at', params.auto_unpause_at) + return `https://api.apollo.io/api/v1/emailer_campaigns/${params.sequence_id.trim()}/add_contact_ids?${qs.toString()}` }, + method: 'POST', + headers: (params: ApolloSequenceAddContactsParams) => ({ + 'Cache-Control': 'no-cache', + 'X-Api-Key': params.apiKey, + }), }, transformResponse: async (response: Response, params?: ApolloSequenceAddContactsParams) => { diff --git a/apps/sim/tools/apollo/sequence_search.ts b/apps/sim/tools/apollo/sequence_search.ts index c7ed1a8b8ad..7fe80e3761b 100644 --- a/apps/sim/tools/apollo/sequence_search.ts +++ b/apps/sim/tools/apollo/sequence_search.ts @@ -14,7 +14,7 @@ export const apolloSequenceSearchTool: ToolConfig< apiKey: { type: 'string', required: true, - visibility: 'hidden', + visibility: 'user-only', description: 'Apollo API key (master key required)', }, q_name: { @@ -38,21 +38,18 @@ export const apolloSequenceSearchTool: ToolConfig< }, request: { - url: 'https://api.apollo.io/api/v1/emailer_campaigns/search', + url: (params: ApolloSequenceSearchParams) => { + const qs = new URLSearchParams() + qs.set('page', String(params.page || 1)) + qs.set('per_page', String(Math.min(params.per_page || 25, 100))) + if (params.q_name) qs.set('q_name', params.q_name) + return `https://api.apollo.io/api/v1/emailer_campaigns/search?${qs.toString()}` + }, method: 'POST', headers: (params: ApolloSequenceSearchParams) => ({ - 'Content-Type': 'application/json', 'Cache-Control': 'no-cache', 'X-Api-Key': params.apiKey, }), - body: (params: ApolloSequenceSearchParams) => { - const body: Record = { - page: params.page || 1, - per_page: Math.min(params.per_page || 25, 100), - } - if (params.q_name) body.q_name = params.q_name - return body - }, }, transformResponse: async (response: Response) => { diff --git a/apps/sim/tools/apollo/task_create.ts b/apps/sim/tools/apollo/task_create.ts index f53b4599e76..5c7d5a6d125 100644 --- a/apps/sim/tools/apollo/task_create.ts +++ b/apps/sim/tools/apollo/task_create.ts @@ -11,7 +11,7 @@ export const apolloTaskCreateTool: ToolConfig { + const qs = new URLSearchParams() + qs.set('page', String(params.page || 1)) + qs.set('per_page', String(Math.min(params.per_page || 25, 100))) + if (params.sort_by_field) qs.set('sort_by_field', params.sort_by_field) + if (params.open_factor_names?.length) { + for (const name of params.open_factor_names) { + if (typeof name === 'string' && name) qs.append('open_factor_names[]', name) + } + } + return `https://api.apollo.io/api/v1/tasks/search?${qs.toString()}` + }, method: 'POST', headers: (params: ApolloTaskSearchParams) => ({ - 'Content-Type': 'application/json', 'Cache-Control': 'no-cache', 'X-Api-Key': params.apiKey, }), - body: (params: ApolloTaskSearchParams) => { - const body: Record = { - page: params.page || 1, - per_page: Math.min(params.per_page || 25, 100), - } - if (params.sort_by_field) body.sort_by_field = params.sort_by_field - if (params.open_factor_names?.length) body.open_factor_names = params.open_factor_names - return body - }, }, transformResponse: async (response: Response) => { diff --git a/apps/sim/tools/apollo/types.ts b/apps/sim/tools/apollo/types.ts index 0dd023467c5..1c68e180ea0 100644 --- a/apps/sim/tools/apollo/types.ts +++ b/apps/sim/tools/apollo/types.ts @@ -229,7 +229,7 @@ export interface ApolloOrganizationEnrichResponse extends ToolResponse { // Bulk Organization Enrichment Types export interface ApolloOrganizationBulkEnrichParams extends ApolloBaseParams { - organizations: Array<{ name: string; domain?: string }> + domains: string[] } export interface ApolloOrganizationBulkEnrichResponse extends ToolResponse { @@ -344,8 +344,10 @@ export interface ApolloContactBulkUpdateParams extends ApolloBaseParams { export interface ApolloContactBulkUpdateResponse extends ToolResponse { output: { - message: string | null + contacts: ApolloContact[] + entity_progress_job: Record | null job_id: string | null + message: string | null } } @@ -466,14 +468,18 @@ export interface ApolloAccountBulkUpdateParams extends ApolloBaseParams { account_ids?: string[] name?: string owner_id?: string + account_stage_id?: string account_attributes?: Array<{ id: string; [key: string]: unknown }> | Record async?: boolean } export interface ApolloAccountBulkUpdateResponse extends ToolResponse { output: { - message: string | null + accounts: ApolloAccount[] account_ids: string[] + entity_progress_job: Record | null + job_id: string | null + message: string | null } } diff --git a/apps/sim/tools/findymail/find_email_from_linkedin.ts b/apps/sim/tools/findymail/find_email_from_linkedin.ts index 8193035fc41..87eb569f960 100644 --- a/apps/sim/tools/findymail/find_email_from_linkedin.ts +++ b/apps/sim/tools/findymail/find_email_from_linkedin.ts @@ -49,6 +49,7 @@ export const findEmailFromLinkedInTool: ToolConfig< success: false, error: (errorData as Record).message || + (errorData as Record).error || `Findymail API error: ${response.status} ${response.statusText}`, output: { contact: null }, } diff --git a/apps/sim/tools/findymail/find_email_from_name.ts b/apps/sim/tools/findymail/find_email_from_name.ts index 048a885e385..5a22a19c3b8 100644 --- a/apps/sim/tools/findymail/find_email_from_name.ts +++ b/apps/sim/tools/findymail/find_email_from_name.ts @@ -54,6 +54,7 @@ export const findEmailFromNameTool: ToolConfig< success: false, error: (errorData as Record).message || + (errorData as Record).error || `Findymail API error: ${response.status} ${response.statusText}`, output: { contact: null }, } diff --git a/apps/sim/tools/findymail/find_emails_by_domain.ts b/apps/sim/tools/findymail/find_emails_by_domain.ts index 5c726c01847..ebb1de7ee3f 100644 --- a/apps/sim/tools/findymail/find_emails_by_domain.ts +++ b/apps/sim/tools/findymail/find_emails_by_domain.ts @@ -55,6 +55,7 @@ export const findEmailsByDomainTool: ToolConfig< success: false, error: (errorData as Record).message || + (errorData as Record).error || `Findymail API error: ${response.status} ${response.statusText}`, output: { contacts: [] }, } diff --git a/apps/sim/tools/findymail/find_employees.ts b/apps/sim/tools/findymail/find_employees.ts index 8cfcf208b51..aabd08ce472 100644 --- a/apps/sim/tools/findymail/find_employees.ts +++ b/apps/sim/tools/findymail/find_employees.ts @@ -68,6 +68,7 @@ export const findEmployeesTool: ToolConfig< success: false, error: (errorData as Record).message || + (errorData as Record).error || `Findymail API error: ${response.status} ${response.statusText}`, output: { employees: [] }, } diff --git a/apps/sim/tools/findymail/find_phone.ts b/apps/sim/tools/findymail/find_phone.ts index 6405401335a..0222eeed796 100644 --- a/apps/sim/tools/findymail/find_phone.ts +++ b/apps/sim/tools/findymail/find_phone.ts @@ -42,6 +42,7 @@ export const findPhoneTool: ToolConfig).message || + (errorData as Record).error || `Findymail API error: ${response.status} ${response.statusText}`, output: { phone: null, line_type: null }, } diff --git a/apps/sim/tools/findymail/get_company.ts b/apps/sim/tools/findymail/get_company.ts index 7a1c10cb244..2ee001f8527 100644 --- a/apps/sim/tools/findymail/get_company.ts +++ b/apps/sim/tools/findymail/get_company.ts @@ -62,6 +62,7 @@ export const getCompanyTool: ToolConfig).message || + (errorData as Record).error || `Findymail API error: ${response.status} ${response.statusText}`, output: { name: null, diff --git a/apps/sim/tools/findymail/get_credits.ts b/apps/sim/tools/findymail/get_credits.ts index 1bdfef5f0b9..7d36549d0af 100644 --- a/apps/sim/tools/findymail/get_credits.ts +++ b/apps/sim/tools/findymail/get_credits.ts @@ -35,6 +35,7 @@ export const getCreditsTool: ToolConfig).message || + (errorData as Record).error || `Findymail API error: ${response.status} ${response.statusText}`, output: { credits: 0, verifier_credits: 0 }, } diff --git a/apps/sim/tools/findymail/lookup_technologies.ts b/apps/sim/tools/findymail/lookup_technologies.ts index a662f220eb6..b7fb2af8d1d 100644 --- a/apps/sim/tools/findymail/lookup_technologies.ts +++ b/apps/sim/tools/findymail/lookup_technologies.ts @@ -61,6 +61,7 @@ export const lookupTechnologiesTool: ToolConfig< success: false, error: (errorData as Record).message || + (errorData as Record).error || `Findymail API error: ${response.status} ${response.statusText}`, output: { domain: '', technologies: [] }, } diff --git a/apps/sim/tools/findymail/reverse_email_lookup.ts b/apps/sim/tools/findymail/reverse_email_lookup.ts index 3808a3a6324..48a0c42ca18 100644 --- a/apps/sim/tools/findymail/reverse_email_lookup.ts +++ b/apps/sim/tools/findymail/reverse_email_lookup.ts @@ -56,6 +56,7 @@ export const reverseEmailLookupTool: ToolConfig< success: false, error: (errorData as Record).message || + (errorData as Record).error || `Findymail API error: ${response.status} ${response.statusText}`, output: { email: null, diff --git a/apps/sim/tools/findymail/search_technologies.ts b/apps/sim/tools/findymail/search_technologies.ts index 1c168c7b35d..a4f7bcadcc5 100644 --- a/apps/sim/tools/findymail/search_technologies.ts +++ b/apps/sim/tools/findymail/search_technologies.ts @@ -50,6 +50,7 @@ export const searchTechnologiesTool: ToolConfig< success: false, error: (errorData as Record).message || + (errorData as Record).error || `Findymail API error: ${response.status} ${response.statusText}`, output: { technologies: [] }, } diff --git a/apps/sim/tools/findymail/verify_email.ts b/apps/sim/tools/findymail/verify_email.ts index ec5d97ad5d4..c239e296cd0 100644 --- a/apps/sim/tools/findymail/verify_email.ts +++ b/apps/sim/tools/findymail/verify_email.ts @@ -44,6 +44,7 @@ export const verifyEmailTool: ToolConfig).message || + (errorData as Record).error || `Findymail API error: ${response.status} ${response.statusText}`, output: { email: '', verified: false, provider: null }, } diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 3133260d9cc..4dd088163e1 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -3176,6 +3176,14 @@ import { zoomListRecordingsTool, zoomUpdateMeetingTool, } from '@/tools/zoom' +import { + zoominfoEnrichCompaniesTool, + zoominfoEnrichContactsTool, + zoominfoSearchCompaniesTool, + zoominfoSearchContactsTool, + zoominfoSearchIntentTool, + zoominfoSearchNewsTool, +} from '@/tools/zoominfo' // Registry of all available tools export const tools: Record = { @@ -4999,17 +5007,17 @@ export const tools: Record = { fathom_get_transcript: fathomGetTranscriptTool, fathom_list_team_members: fathomListTeamMembersTool, fathom_list_teams: fathomListTeamsTool, - findymail_verify_email: findymailVerifyEmailTool, + findymail_find_email_from_linkedin: findymailFindEmailFromLinkedInTool, findymail_find_email_from_name: findymailFindEmailFromNameTool, findymail_find_emails_by_domain: findymailFindEmailsByDomainTool, - findymail_find_email_from_linkedin: findymailFindEmailFromLinkedInTool, - findymail_reverse_email_lookup: findymailReverseEmailLookupTool, - findymail_get_company: findymailGetCompanyTool, findymail_find_employees: findymailFindEmployeesTool, findymail_find_phone: findymailFindPhoneTool, - findymail_search_technologies: findymailSearchTechnologiesTool, - findymail_lookup_technologies: findymailLookupTechnologiesTool, + findymail_get_company: findymailGetCompanyTool, findymail_get_credits: findymailGetCreditsTool, + findymail_lookup_technologies: findymailLookupTechnologiesTool, + findymail_reverse_email_lookup: findymailReverseEmailLookupTool, + findymail_search_technologies: findymailSearchTechnologiesTool, + findymail_verify_email: findymailVerifyEmailTool, stt_whisper: whisperSttTool, stt_whisper_v2: whisperSttV2Tool, stt_deepgram: deepgramSttTool, @@ -6006,6 +6014,12 @@ export const tools: Record = { zoom_get_meeting_recordings: zoomGetMeetingRecordingsTool, zoom_delete_recording: zoomDeleteRecordingTool, zoom_list_past_participants: zoomListPastParticipantsTool, + zoominfo_search_companies: zoominfoSearchCompaniesTool, + zoominfo_search_contacts: zoominfoSearchContactsTool, + zoominfo_enrich_companies: zoominfoEnrichCompaniesTool, + zoominfo_enrich_contacts: zoominfoEnrichContactsTool, + zoominfo_search_intent: zoominfoSearchIntentTool, + zoominfo_search_news: zoominfoSearchNewsTool, // Spotify spotify_search: spotifySearchTool, spotify_get_track: spotifyGetTrackTool, diff --git a/apps/sim/tools/wiza/company_enrichment.ts b/apps/sim/tools/wiza/company_enrichment.ts index 75201dd694c..70bfdfec5be 100644 --- a/apps/sim/tools/wiza/company_enrichment.ts +++ b/apps/sim/tools/wiza/company_enrichment.ts @@ -137,6 +137,10 @@ export const wizaCompanyEnrichmentTool: ToolConfig< company_region: { type: 'string', description: 'State/region', optional: true }, company_postal_code: { type: 'string', description: 'Postal code', optional: true }, company_country: { type: 'string', description: 'Country', optional: true }, - credits: { type: 'json', description: 'Remaining API credits', optional: true }, + credits: { + type: 'json', + description: 'Credits deducted for this enrichment (api_credits: { total, company_credits })', + optional: true, + }, }, } diff --git a/apps/sim/tools/wiza/get_individual_reveal.ts b/apps/sim/tools/wiza/get_individual_reveal.ts index 52661bb8482..7c3d4ec5434 100644 --- a/apps/sim/tools/wiza/get_individual_reveal.ts +++ b/apps/sim/tools/wiza/get_individual_reveal.ts @@ -100,9 +100,17 @@ export const wizaGetIndividualRevealTool: ToolConfig< }, outputs: { - id: { type: 'number', description: 'Reveal ID' }, - status: { type: 'string', description: 'queued | resolving | finished | failed' }, - is_complete: { type: 'boolean', description: 'Whether the reveal has completed' }, + id: { type: 'number', description: 'Reveal ID', optional: true }, + status: { + type: 'string', + description: 'queued | resolving | finished | failed', + optional: true, + }, + is_complete: { + type: 'boolean', + description: 'Whether the reveal has completed', + optional: true, + }, name: { type: 'string', description: 'Full name', optional: true }, company: { type: 'string', description: 'Company name', optional: true }, enrichment_level: { type: 'string', description: 'Enrichment level used', optional: true }, @@ -158,6 +166,11 @@ export const wizaGetIndividualRevealTool: ToolConfig< company_linkedin: { type: 'string', description: 'Company LinkedIn URL', optional: true }, company_location: { type: 'string', description: 'Full company location', optional: true }, company_description: { type: 'string', description: 'Company description', optional: true }, - credits: { type: 'json', description: 'Remaining credits balance', optional: true }, + credits: { + type: 'json', + description: + 'Credits deducted for this reveal (api_credits: { total, email_credits, phone_credits, scrape_credits })', + optional: true, + }, }, } diff --git a/apps/sim/tools/wiza/start_individual_reveal.ts b/apps/sim/tools/wiza/start_individual_reveal.ts index 10955e4c548..902c51f8723 100644 --- a/apps/sim/tools/wiza/start_individual_reveal.ts +++ b/apps/sim/tools/wiza/start_individual_reveal.ts @@ -132,11 +132,20 @@ export const wizaStartIndividualRevealTool: ToolConfig< }, outputs: { - id: { type: 'number', description: 'Individual reveal ID (use with Get Individual Reveal)' }, + id: { + type: 'number', + description: 'Individual reveal ID (use with Get Individual Reveal)', + optional: true, + }, status: { type: 'string', description: 'Reveal status: queued, resolving, finished, or failed', + optional: true, + }, + is_complete: { + type: 'boolean', + description: 'Whether the reveal has completed', + optional: true, }, - is_complete: { type: 'boolean', description: 'Whether the reveal has completed' }, }, } diff --git a/apps/sim/tools/wiza/types.ts b/apps/sim/tools/wiza/types.ts index f75e794f622..00db0a23563 100644 --- a/apps/sim/tools/wiza/types.ts +++ b/apps/sim/tools/wiza/types.ts @@ -13,25 +13,36 @@ export interface WizaGetCreditsResponse extends ToolResponse { } } +export interface WizaIncludeExcludeFilter { + v: string + s: 'i' | 'e' +} + +export interface WizaLocationFilter { + v: string | { country?: string; state?: string; city?: string } + b: 'country' | 'state' | 'city' + s: 'i' | 'e' +} + export interface WizaProspectSearchParams { apiKey: string size?: number filters?: Record first_name?: string[] last_name?: string[] - job_title?: unknown[] + job_title?: WizaIncludeExcludeFilter[] job_title_level?: string[] job_role?: string[] job_sub_role?: string[] - location?: unknown[] + location?: WizaLocationFilter[] skill?: string[] school?: string[] major?: string[] linkedin_slug?: string[] - job_company?: unknown[] - past_company?: unknown[] - company_location?: unknown[] - company_industry?: unknown[] + job_company?: WizaIncludeExcludeFilter[] + past_company?: WizaIncludeExcludeFilter[] + company_location?: WizaLocationFilter[] + company_industry?: WizaIncludeExcludeFilter[] company_size?: string[] company_type?: string[] } diff --git a/apps/sim/tools/zoominfo/enrich_companies.ts b/apps/sim/tools/zoominfo/enrich_companies.ts new file mode 100644 index 00000000000..b3b00fd8b90 --- /dev/null +++ b/apps/sim/tools/zoominfo/enrich_companies.ts @@ -0,0 +1,136 @@ +import type { ToolConfig } from '@/tools/types' +import type { + ZoomInfoEnrichCompaniesParams, + ZoomInfoEnrichCompaniesResponse, +} from '@/tools/zoominfo/types' +import { + buildProxyBody, + extractDataArray, + parseCsvOrJson, + parseJsonField, + transformZoomInfoEnvelope, + ZOOMINFO_PROXY_URL, +} from '@/tools/zoominfo/utils' + +/** + * Default output fields used when the caller does not specify any. ZoomInfo's + * CompanyEnrich schema requires `outputFields`, so we send a useful firmographic + * set rather than letting the request fail. All values are valid CompanyEnrich fields. + */ +const DEFAULT_COMPANY_OUTPUT_FIELDS = [ + 'id', + 'name', + 'website', + 'domainList', + 'ticker', + 'revenue', + 'revenueRange', + 'employeeCount', + 'employeeRange', + 'primaryIndustry', + 'industries', + 'street', + 'city', + 'state', + 'zipCode', + 'country', + 'phone', + 'foundedYear', + 'companyStatus', + 'socialMediaUrls', + 'logo', + 'description', +] + +export const zoominfoEnrichCompaniesTool: ToolConfig< + ZoomInfoEnrichCompaniesParams, + ZoomInfoEnrichCompaniesResponse +> = { + id: 'zoominfo_enrich_companies', + name: 'ZoomInfo Enrich Companies', + description: + 'Enrich up to 25 companies in one request with detailed firmographics, industry, financials, and more.', + version: '1.0.0', + + params: { + clientId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'ZoomInfo OAuth client ID', + }, + clientSecret: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'ZoomInfo OAuth client secret', + }, + matchCompanyInput: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'JSON array (1-25 items) of company matching criteria, e.g. [{"companyName":"Acme","companyWebsite":"acme.com"}]', + }, + outputFields: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'JSON array or comma-separated list of fields to return (e.g. ["id","name","website","revenue","employeeCount"]). Defaults to a standard firmographic set if omitted.', + }, + }, + + request: { + url: ZOOMINFO_PROXY_URL, + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => { + const matchCompanyInput = parseJsonField( + params.matchCompanyInput, + 'matchCompanyInput' + ) + if (!Array.isArray(matchCompanyInput) || matchCompanyInput.length === 0) { + throw new Error('matchCompanyInput must be a non-empty JSON array') + } + if (matchCompanyInput.length > 25) { + throw new Error('matchCompanyInput supports a maximum of 25 entries per request') + } + + const outputFields = parseCsvOrJson(params.outputFields, 'outputFields') + const attributes: Record = { + matchCompanyInput, + outputFields: outputFields ?? DEFAULT_COMPANY_OUTPUT_FIELDS, + } + + return { + ...buildProxyBody(params), + path: '/data/v1/companies/enrich', + method: 'POST', + body: { + data: { + type: 'CompanyEnrich', + attributes, + }, + }, + } + }, + }, + + transformResponse: async (response: Response) => { + const { data } = await transformZoomInfoEnvelope(response) + const results = extractDataArray(data) + return { + success: true, + output: { results }, + } + }, + + outputs: { + results: { + type: 'array', + description: 'Enrichment results, one per input with match status and attributes', + items: { type: 'json' }, + }, + }, +} diff --git a/apps/sim/tools/zoominfo/enrich_contacts.ts b/apps/sim/tools/zoominfo/enrich_contacts.ts new file mode 100644 index 00000000000..bdfe9ea02f8 --- /dev/null +++ b/apps/sim/tools/zoominfo/enrich_contacts.ts @@ -0,0 +1,139 @@ +import type { ToolConfig } from '@/tools/types' +import type { + ZoomInfoEnrichContactsParams, + ZoomInfoEnrichContactsResponse, +} from '@/tools/zoominfo/types' +import { + buildProxyBody, + extractDataArray, + parseCsvOrJson, + parseJsonField, + transformZoomInfoEnvelope, + ZOOMINFO_PROXY_URL, +} from '@/tools/zoominfo/utils' + +/** + * Default output fields used when the caller does not specify any. ZoomInfo's + * ContactEnrich schema requires `outputFields`, so we send a useful contact set + * rather than letting the request fail. All values are valid ContactEnrich fields. + */ +const DEFAULT_CONTACT_OUTPUT_FIELDS = [ + 'id', + 'firstName', + 'lastName', + 'email', + 'phone', + 'mobilePhone', + 'jobTitle', + 'jobFunction', + 'managementLevel', + 'city', + 'state', + 'country', + 'contactAccuracyScore', + 'validDate', + 'lastUpdatedDate', + 'companyId', + 'companyName', + 'companyWebsite', + 'companyPhone', +] + +export const zoominfoEnrichContactsTool: ToolConfig< + ZoomInfoEnrichContactsParams, + ZoomInfoEnrichContactsResponse +> = { + id: 'zoominfo_enrich_contacts', + name: 'ZoomInfo Enrich Contacts', + description: + 'Enrich up to 25 contacts in one request with verified emails, phone numbers, job details, and more.', + version: '1.0.0', + + params: { + clientId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'ZoomInfo OAuth client ID', + }, + clientSecret: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'ZoomInfo OAuth client secret', + }, + matchPersonInput: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'JSON array (1-25 items) of contact matching criteria, e.g. [{"firstName":"Jane","lastName":"Doe","companyName":"Acme"}]', + }, + outputFields: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'JSON array or comma-separated list of fields to return (e.g. ["id","firstName","email","phone","jobTitle"]). Defaults to a standard contact set if omitted.', + }, + requiredFields: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'JSON array or comma-separated list of fields that must exist in results (e.g. ["email"])', + }, + }, + + request: { + url: ZOOMINFO_PROXY_URL, + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => { + const matchPersonInput = parseJsonField(params.matchPersonInput, 'matchPersonInput') + if (!Array.isArray(matchPersonInput) || matchPersonInput.length === 0) { + throw new Error('matchPersonInput must be a non-empty JSON array') + } + if (matchPersonInput.length > 25) { + throw new Error('matchPersonInput supports a maximum of 25 entries per request') + } + + const outputFields = parseCsvOrJson(params.outputFields, 'outputFields') + const attributes: Record = { + matchPersonInput, + outputFields: outputFields ?? DEFAULT_CONTACT_OUTPUT_FIELDS, + } + const requiredFields = parseCsvOrJson(params.requiredFields, 'requiredFields') + if (requiredFields) attributes.requiredFields = requiredFields + + return { + ...buildProxyBody(params), + path: '/data/v1/contacts/enrich', + method: 'POST', + body: { + data: { + type: 'ContactEnrich', + attributes, + }, + }, + } + }, + }, + + transformResponse: async (response: Response) => { + const { data } = await transformZoomInfoEnvelope(response) + const results = extractDataArray(data) + return { + success: true, + output: { results }, + } + }, + + outputs: { + results: { + type: 'array', + description: 'Enrichment results, one per input with match status and attributes', + items: { type: 'json' }, + }, + }, +} diff --git a/apps/sim/tools/zoominfo/index.ts b/apps/sim/tools/zoominfo/index.ts new file mode 100644 index 00000000000..ecd29d40751 --- /dev/null +++ b/apps/sim/tools/zoominfo/index.ts @@ -0,0 +1,6 @@ +export { zoominfoEnrichCompaniesTool } from '@/tools/zoominfo/enrich_companies' +export { zoominfoEnrichContactsTool } from '@/tools/zoominfo/enrich_contacts' +export { zoominfoSearchCompaniesTool } from '@/tools/zoominfo/search_companies' +export { zoominfoSearchContactsTool } from '@/tools/zoominfo/search_contacts' +export { zoominfoSearchIntentTool } from '@/tools/zoominfo/search_intent' +export { zoominfoSearchNewsTool } from '@/tools/zoominfo/search_news' diff --git a/apps/sim/tools/zoominfo/search_companies.ts b/apps/sim/tools/zoominfo/search_companies.ts new file mode 100644 index 00000000000..e2a33c943e9 --- /dev/null +++ b/apps/sim/tools/zoominfo/search_companies.ts @@ -0,0 +1,213 @@ +import type { ToolConfig } from '@/tools/types' +import type { + ZoomInfoSearchCompaniesParams, + ZoomInfoSearchCompaniesResponse, +} from '@/tools/zoominfo/types' +import { + buildProxyBody, + extractDataArray, + extractPagination, + paginationOutputProperties, + parseCsvOrJson, + toCsvStringOrUndefined, + toNumberOrUndefined, + transformZoomInfoEnvelope, + ZOOMINFO_PROXY_URL, +} from '@/tools/zoominfo/utils' + +export const zoominfoSearchCompaniesTool: ToolConfig< + ZoomInfoSearchCompaniesParams, + ZoomInfoSearchCompaniesResponse +> = { + id: 'zoominfo_search_companies', + name: 'ZoomInfo Search Companies', + description: 'Search the ZoomInfo company database by name, industry, location, and size.', + version: '1.0.0', + + params: { + clientId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'ZoomInfo OAuth client ID', + }, + clientSecret: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'ZoomInfo OAuth client secret', + }, + companyName: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Company name to search for', + }, + companyWebsite: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Company website (comma-separated for multiple)', + }, + companyTicker: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Stock ticker symbols — JSON array, comma-separated list, or single ticker. Sent to the API as an array.', + }, + industryCodes: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Industry codes — JSON array or comma-separated list. Sent to the API as a comma-separated string.', + }, + country: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Country name', + }, + state: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'State or province', + }, + metroRegion: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'US/Canada metro region', + }, + revenueMin: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Minimum annual revenue in thousands USD', + }, + revenueMax: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum annual revenue in thousands USD', + }, + employeeRangeMin: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Minimum employee count', + }, + employeeRangeMax: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum employee count', + }, + excludeDefunctCompanies: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Exclude inactive companies', + }, + page: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Page number (1-based)', + }, + rpp: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Results per page (1-100, default 25)', + }, + sortBy: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Field to sort by', + }, + sortOrder: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Sort order (asc or desc)', + }, + }, + + request: { + url: ZOOMINFO_PROXY_URL, + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => { + const attributes: Record = {} + if (params.companyName) attributes.companyName = params.companyName + if (params.companyWebsite) attributes.companyWebsite = params.companyWebsite + const companyTicker = parseCsvOrJson(params.companyTicker, 'companyTicker') + if (companyTicker) attributes.companyTicker = companyTicker + const industryCodes = toCsvStringOrUndefined(params.industryCodes, 'industryCodes') + if (industryCodes) attributes.industryCodes = industryCodes + if (params.country) attributes.country = params.country + if (params.state) attributes.state = params.state + if (params.metroRegion) attributes.metroRegion = params.metroRegion + const revenueMin = toNumberOrUndefined(params.revenueMin) + if (revenueMin !== undefined) attributes.revenueMin = revenueMin + const revenueMax = toNumberOrUndefined(params.revenueMax) + if (revenueMax !== undefined) attributes.revenueMax = revenueMax + const employeeRangeMin = toNumberOrUndefined(params.employeeRangeMin) + if (employeeRangeMin !== undefined) attributes.employeeRangeMin = String(employeeRangeMin) + const employeeRangeMax = toNumberOrUndefined(params.employeeRangeMax) + if (employeeRangeMax !== undefined) attributes.employeeRangeMax = String(employeeRangeMax) + if (params.excludeDefunctCompanies !== undefined) { + attributes.excludeDefunctCompanies = params.excludeDefunctCompanies + } + + const query: Record = {} + const page = toNumberOrUndefined(params.page) + const rpp = toNumberOrUndefined(params.rpp) + if (page !== undefined) query['page[number]'] = page + if (rpp !== undefined) query['page[size]'] = rpp + if (params.sortBy) { + const order = params.sortOrder === 'desc' ? '-' : '' + query.sort = `${order}${params.sortBy}` + } + + return { + ...buildProxyBody(params), + path: '/data/v1/companies/search', + method: 'POST', + query: Object.keys(query).length > 0 ? query : undefined, + body: { + data: { + type: 'CompanySearch', + attributes, + }, + }, + } + }, + }, + + transformResponse: async (response: Response) => { + const { data } = await transformZoomInfoEnvelope(response) + const companies = extractDataArray(data) + const pagination = extractPagination(data) + return { + success: true, + output: { + companies, + ...pagination, + }, + } + }, + + outputs: { + companies: { + type: 'array', + description: 'Matching companies', + items: { type: 'json' }, + }, + ...paginationOutputProperties, + }, +} diff --git a/apps/sim/tools/zoominfo/search_contacts.ts b/apps/sim/tools/zoominfo/search_contacts.ts new file mode 100644 index 00000000000..625af356e76 --- /dev/null +++ b/apps/sim/tools/zoominfo/search_contacts.ts @@ -0,0 +1,212 @@ +import type { ToolConfig } from '@/tools/types' +import type { + ZoomInfoSearchContactsParams, + ZoomInfoSearchContactsResponse, +} from '@/tools/zoominfo/types' +import { + buildProxyBody, + extractDataArray, + extractPagination, + paginationOutputProperties, + toCsvStringOrUndefined, + toNumberOrUndefined, + transformZoomInfoEnvelope, + ZOOMINFO_PROXY_URL, +} from '@/tools/zoominfo/utils' + +export const zoominfoSearchContactsTool: ToolConfig< + ZoomInfoSearchContactsParams, + ZoomInfoSearchContactsResponse +> = { + id: 'zoominfo_search_contacts', + name: 'ZoomInfo Search Contacts', + description: + 'Search ZoomInfo for contacts (people) by name, job title, company, and other filters. Does not return emails or phone numbers — use Enrich Contacts for engagement data.', + version: '1.0.0', + + params: { + clientId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'ZoomInfo OAuth client ID', + }, + clientSecret: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'ZoomInfo OAuth client secret', + }, + firstName: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'First name', + }, + lastName: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Last name', + }, + fullName: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Full name', + }, + emailAddress: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Email address', + }, + jobTitle: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Job title', + }, + managementLevel: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Management level — JSON array or comma-separated list. Sent to the API as a comma-separated string.', + }, + department: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Department — JSON array or comma-separated list. Sent to the API as a comma-separated string.', + }, + companyId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'ZoomInfo company ID', + }, + companyName: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Company name', + }, + contactAccuracyScoreMin: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Minimum accuracy score (70-99)', + }, + requiredFields: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Fields that must exist in results — JSON array or comma-separated list. Sent to the API as a comma-separated string.', + }, + excludePartialProfiles: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Exclude partial profiles', + }, + page: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Page number (1-based)', + }, + rpp: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Results per page (1-100, default 25)', + }, + sortBy: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Field to sort by', + }, + sortOrder: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Sort order (asc or desc)', + }, + }, + + request: { + url: ZOOMINFO_PROXY_URL, + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => { + const attributes: Record = {} + if (params.firstName) attributes.firstName = params.firstName + if (params.lastName) attributes.lastName = params.lastName + if (params.fullName) attributes.fullName = params.fullName + if (params.emailAddress) attributes.emailAddress = params.emailAddress + if (params.jobTitle) attributes.jobTitle = params.jobTitle + const managementLevel = toCsvStringOrUndefined(params.managementLevel, 'managementLevel') + if (managementLevel) attributes.managementLevel = managementLevel + const department = toCsvStringOrUndefined(params.department, 'department') + if (department) attributes.department = department + if (params.companyId) attributes.companyId = params.companyId + if (params.companyName) attributes.companyName = params.companyName + const minScore = toNumberOrUndefined(params.contactAccuracyScoreMin) + if (minScore !== undefined) attributes.contactAccuracyScoreMin = String(minScore) + const required = toCsvStringOrUndefined(params.requiredFields, 'requiredFields') + if (required) attributes.requiredFields = required + if (params.excludePartialProfiles !== undefined) { + attributes.excludePartialProfiles = params.excludePartialProfiles + } + + const query: Record = {} + const page = toNumberOrUndefined(params.page) + const rpp = toNumberOrUndefined(params.rpp) + if (page !== undefined) query['page[number]'] = page + if (rpp !== undefined) query['page[size]'] = rpp + if (params.sortBy) { + const order = params.sortOrder === 'desc' ? '-' : '' + query.sort = `${order}${params.sortBy}` + } + + return { + ...buildProxyBody(params), + path: '/data/v1/contacts/search', + method: 'POST', + query: Object.keys(query).length > 0 ? query : undefined, + body: { + data: { + type: 'ContactSearch', + attributes, + }, + }, + } + }, + }, + + transformResponse: async (response: Response) => { + const { data } = await transformZoomInfoEnvelope(response) + const contacts = extractDataArray(data) + const pagination = extractPagination(data) + return { + success: true, + output: { + contacts, + ...pagination, + }, + } + }, + + outputs: { + contacts: { + type: 'array', + description: 'Matching contacts (without emails or phone numbers)', + items: { type: 'json' }, + }, + ...paginationOutputProperties, + }, +} diff --git a/apps/sim/tools/zoominfo/search_intent.ts b/apps/sim/tools/zoominfo/search_intent.ts new file mode 100644 index 00000000000..e56a2a488f4 --- /dev/null +++ b/apps/sim/tools/zoominfo/search_intent.ts @@ -0,0 +1,190 @@ +import type { ToolConfig } from '@/tools/types' +import type { + ZoomInfoSearchIntentParams, + ZoomInfoSearchIntentResponse, +} from '@/tools/zoominfo/types' +import { + buildProxyBody, + extractDataArray, + extractPagination, + paginationOutputProperties, + parseCsvOrJson, + toCsvStringOrUndefined, + toNumberOrUndefined, + transformZoomInfoEnvelope, + ZOOMINFO_PROXY_URL, +} from '@/tools/zoominfo/utils' + +export const zoominfoSearchIntentTool: ToolConfig< + ZoomInfoSearchIntentParams, + ZoomInfoSearchIntentResponse +> = { + id: 'zoominfo_search_intent', + name: 'ZoomInfo Search Intent', + description: 'Search for companies showing intent signals on specific topics.', + version: '1.0.0', + + params: { + clientId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'ZoomInfo OAuth client ID', + }, + clientSecret: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'ZoomInfo OAuth client secret', + }, + topics: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'Up to 50 intent topics as JSON array or comma-separated list (e.g. ["CRM Software","Marketing Automation"])', + }, + signalStartDate: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Earliest signal date (YYYY-MM-DD)', + }, + signalEndDate: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Latest signal date (YYYY-MM-DD)', + }, + signalScoreMin: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Minimum signal score (60-100)', + }, + signalScoreMax: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum signal score (60-100)', + }, + audienceStrengthMin: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Minimum audience strength (A-E, A is largest)', + }, + audienceStrengthMax: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Maximum audience strength (A-E, A is largest)', + }, + findRecommendedContacts: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Include recommended contacts (default true)', + }, + country: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Country filter', + }, + state: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'State filter', + }, + industryCodes: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Industry codes — JSON array or comma-separated list. Sent to the API as a comma-separated string.', + }, + page: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Page number (1-based)', + }, + rpp: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Results per page (1-100, default 25)', + }, + }, + + request: { + url: ZOOMINFO_PROXY_URL, + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => { + const topics = parseCsvOrJson(params.topics, 'topics') + if (!topics || topics.length === 0) { + throw new Error('topics is required') + } + const attributes: Record = { topics } + if (params.signalStartDate) attributes.signalStartDate = params.signalStartDate + if (params.signalEndDate) attributes.signalEndDate = params.signalEndDate + const scoreMin = toNumberOrUndefined(params.signalScoreMin) + if (scoreMin !== undefined) attributes.signalScoreMin = scoreMin + const scoreMax = toNumberOrUndefined(params.signalScoreMax) + if (scoreMax !== undefined) attributes.signalScoreMax = scoreMax + if (params.audienceStrengthMin) attributes.audienceStrengthMin = params.audienceStrengthMin + if (params.audienceStrengthMax) attributes.audienceStrengthMax = params.audienceStrengthMax + if (params.findRecommendedContacts !== undefined) { + attributes.findRecommendedContacts = params.findRecommendedContacts + } + if (params.country) attributes.country = params.country + if (params.state) attributes.state = params.state + const industryCodes = toCsvStringOrUndefined(params.industryCodes, 'industryCodes') + if (industryCodes) attributes.industryCodes = industryCodes + + const query: Record = {} + const page = toNumberOrUndefined(params.page) + const rpp = toNumberOrUndefined(params.rpp) + if (page !== undefined) query['page[number]'] = page + if (rpp !== undefined) query['page[size]'] = rpp + + return { + ...buildProxyBody(params), + path: '/data/v1/intent/search', + method: 'POST', + query: Object.keys(query).length > 0 ? query : undefined, + body: { + data: { + type: 'IntentSearch', + attributes, + }, + }, + } + }, + }, + + transformResponse: async (response: Response) => { + const { data } = await transformZoomInfoEnvelope(response) + const signals = extractDataArray(data) + const pagination = extractPagination(data) + return { + success: true, + output: { + signals, + ...pagination, + }, + } + }, + + outputs: { + signals: { + type: 'array', + description: 'Intent signals with topic, score, audience strength, and company', + items: { type: 'json' }, + }, + ...paginationOutputProperties, + }, +} diff --git a/apps/sim/tools/zoominfo/search_news.ts b/apps/sim/tools/zoominfo/search_news.ts new file mode 100644 index 00000000000..413b6e52ea3 --- /dev/null +++ b/apps/sim/tools/zoominfo/search_news.ts @@ -0,0 +1,133 @@ +import type { ToolConfig } from '@/tools/types' +import type { ZoomInfoSearchNewsParams, ZoomInfoSearchNewsResponse } from '@/tools/zoominfo/types' +import { + buildProxyBody, + extractDataArray, + extractPagination, + paginationOutputProperties, + parseCsvOrJson, + toNumberOrUndefined, + transformZoomInfoEnvelope, + ZOOMINFO_PROXY_URL, +} from '@/tools/zoominfo/utils' + +export const zoominfoSearchNewsTool: ToolConfig< + ZoomInfoSearchNewsParams, + ZoomInfoSearchNewsResponse +> = { + id: 'zoominfo_search_news', + name: 'ZoomInfo Search News', + description: 'Search ZoomInfo news articles by category, URL, or date range.', + version: '1.0.0', + + params: { + clientId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'ZoomInfo OAuth client ID', + }, + clientSecret: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'ZoomInfo OAuth client secret', + }, + categories: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'News categories as JSON array or comma-separated list', + }, + url: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'News source URLs as JSON array or comma-separated list', + }, + pageDateMin: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Earliest publish date (YYYY-MM-DD)', + }, + pageDateMax: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Latest publish date (YYYY-MM-DD)', + }, + page: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Page number (1-based)', + }, + rpp: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Results per page (1-100, default 25)', + }, + }, + + request: { + url: ZOOMINFO_PROXY_URL, + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => { + const attributes: Record = {} + const categories = parseCsvOrJson(params.categories, 'categories') + if (categories) attributes.categories = categories + const urls = parseCsvOrJson(params.url, 'url') + if (urls) attributes.url = urls + if (params.pageDateMin) attributes.pageDateMin = params.pageDateMin + if (params.pageDateMax) attributes.pageDateMax = params.pageDateMax + + if (Object.keys(attributes).length === 0) { + throw new Error('Provide at least one of: categories, url, pageDateMin, pageDateMax') + } + + const query: Record = {} + const page = toNumberOrUndefined(params.page) + const rpp = toNumberOrUndefined(params.rpp) + if (page !== undefined) query['page[number]'] = page + if (rpp !== undefined) query['page[size]'] = rpp + + return { + ...buildProxyBody(params), + path: '/data/v1/news/search', + method: 'POST', + query: Object.keys(query).length > 0 ? query : undefined, + body: { + data: { + type: 'NewsSearch', + attributes, + }, + }, + } + }, + }, + + transformResponse: async (response: Response) => { + const { data } = await transformZoomInfoEnvelope(response) + const articles = extractDataArray(data) + const pagination = extractPagination(data) + return { + success: true, + output: { + articles, + ...pagination, + }, + } + }, + + outputs: { + articles: { + type: 'array', + description: 'News articles matching the filters', + items: { type: 'json' }, + }, + ...paginationOutputProperties, + }, +} diff --git a/apps/sim/tools/zoominfo/types.ts b/apps/sim/tools/zoominfo/types.ts new file mode 100644 index 00000000000..57a867a4f06 --- /dev/null +++ b/apps/sim/tools/zoominfo/types.ts @@ -0,0 +1,136 @@ +import type { ToolResponse } from '@/tools/types' + +export interface ZoomInfoBaseParams { + clientId: string + clientSecret: string +} + +export interface ZoomInfoSearchCompaniesParams extends ZoomInfoBaseParams { + companyName?: string + companyWebsite?: string + companyTicker?: string + industryCodes?: string + country?: string + state?: string + metroRegion?: string + revenueMin?: number + revenueMax?: number + employeeRangeMin?: number + employeeRangeMax?: number + excludeDefunctCompanies?: boolean + page?: number + rpp?: number + sortBy?: string + sortOrder?: string +} + +export interface ZoomInfoSearchContactsParams extends ZoomInfoBaseParams { + firstName?: string + lastName?: string + fullName?: string + emailAddress?: string + jobTitle?: string + managementLevel?: string + department?: string + companyId?: string + companyName?: string + contactAccuracyScoreMin?: number + requiredFields?: string + excludePartialProfiles?: boolean + page?: number + rpp?: number + sortBy?: string + sortOrder?: string +} + +export interface ZoomInfoEnrichCompaniesParams extends ZoomInfoBaseParams { + matchCompanyInput: string + outputFields?: string +} + +export interface ZoomInfoEnrichContactsParams extends ZoomInfoBaseParams { + matchPersonInput: string + outputFields?: string + requiredFields?: string +} + +export interface ZoomInfoSearchIntentParams extends ZoomInfoBaseParams { + topics: string + signalStartDate?: string + signalEndDate?: string + signalScoreMin?: number + signalScoreMax?: number + audienceStrengthMin?: string + audienceStrengthMax?: string + findRecommendedContacts?: boolean + country?: string + state?: string + industryCodes?: string + page?: number + rpp?: number +} + +export interface ZoomInfoSearchNewsParams extends ZoomInfoBaseParams { + categories?: string + url?: string + pageDateMin?: string + pageDateMax?: string + page?: number + rpp?: number +} + +export interface ZoomInfoSearchCompaniesResponse extends ToolResponse { + output: { + companies: Array> + totalResults: number | null + currentPage: number | null + totalPages: number | null + } +} + +export interface ZoomInfoSearchContactsResponse extends ToolResponse { + output: { + contacts: Array> + totalResults: number | null + currentPage: number | null + totalPages: number | null + } +} + +export interface ZoomInfoEnrichCompaniesResponse extends ToolResponse { + output: { + results: Array> + } +} + +export interface ZoomInfoEnrichContactsResponse extends ToolResponse { + output: { + results: Array> + } +} + +export interface ZoomInfoSearchIntentResponse extends ToolResponse { + output: { + signals: Array> + totalResults: number | null + currentPage: number | null + totalPages: number | null + } +} + +export interface ZoomInfoSearchNewsResponse extends ToolResponse { + output: { + articles: Array> + totalResults: number | null + currentPage: number | null + totalPages: number | null + } +} + +export type ZoomInfoResponse = + | ZoomInfoSearchCompaniesResponse + | ZoomInfoSearchContactsResponse + | ZoomInfoEnrichCompaniesResponse + | ZoomInfoEnrichContactsResponse + | ZoomInfoSearchIntentResponse + | ZoomInfoSearchNewsResponse diff --git a/apps/sim/tools/zoominfo/utils.ts b/apps/sim/tools/zoominfo/utils.ts new file mode 100644 index 00000000000..fa382572d45 --- /dev/null +++ b/apps/sim/tools/zoominfo/utils.ts @@ -0,0 +1,136 @@ +import { getErrorMessage } from '@sim/utils/errors' +import type { OutputProperty } from '@/tools/types' +import type { ZoomInfoBaseParams } from '@/tools/zoominfo/types' + +export const ZOOMINFO_PROXY_URL = '/api/tools/zoominfo/proxy' + +export interface ZoomInfoProxyEnvelope { + clientId: string + clientSecret: string + path: string + method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' + query?: Record + body?: unknown +} + +export function buildProxyBody( + params: ZoomInfoBaseParams +): Pick { + return { + clientId: params.clientId, + clientSecret: params.clientSecret, + } +} + +export function parseJsonField(value: unknown, fieldName: string): T { + if (typeof value !== 'string') return value as T + const trimmed = value.trim() + if (!trimmed) { + throw new Error(`${fieldName} is required`) + } + try { + return JSON.parse(trimmed) as T + } catch (error) { + throw new Error(`${fieldName} must be valid JSON: ${getErrorMessage(error)}`) + } +} + +/** + * Normalize a JSON-array string, real array, or comma-separated string into a + * single comma-separated string. Use for ZoomInfo attributes that the docs + * describe as a scalar string accepting a comma-separated list + * (e.g. industryCodes, managementLevel, department). + */ +export function toCsvStringOrUndefined(value: unknown, fieldName: string): string | undefined { + const arr = parseCsvOrJson(value, fieldName) + if (!arr || arr.length === 0) return undefined + return arr.join(',') +} + +export function parseCsvOrJson(value: unknown, fieldName: string): string[] | undefined { + if (value === undefined || value === null) return undefined + if (Array.isArray(value)) return value.map(String) + if (typeof value !== 'string') return undefined + const trimmed = value.trim() + if (!trimmed) return undefined + if (trimmed.startsWith('[')) { + try { + const parsed = JSON.parse(trimmed) + if (!Array.isArray(parsed)) { + throw new Error(`${fieldName} JSON must be an array of strings`) + } + return parsed.map(String) + } catch (error) { + throw new Error(`${fieldName} must be valid JSON: ${getErrorMessage(error)}`) + } + } + return trimmed + .split(',') + .map((s) => s.trim()) + .filter(Boolean) +} + +export function toNumberOrUndefined(value: unknown): number | undefined { + if (value === undefined || value === null || value === '') return undefined + const n = Number(value) + return Number.isFinite(n) ? n : undefined +} + +export async function transformZoomInfoEnvelope( + response: Response +): Promise<{ status: number; data: unknown }> { + const data = (await response.json()) as + | { success: true; output: { status: number; data: unknown } } + | { success: false; error?: string; status?: number } + if (!('success' in data) || data.success === false) { + const errMessage = 'error' in data && data.error ? data.error : 'ZoomInfo request failed' + throw new Error(errMessage) + } + return { status: data.output.status, data: data.output.data } +} + +export const paginationOutputProperties: Record = { + totalResults: { + type: 'number', + description: 'Total number of matching results across all pages', + optional: true, + }, + currentPage: { + type: 'number', + description: 'Current page number', + optional: true, + }, + totalPages: { + type: 'number', + description: 'Total number of pages available', + optional: true, + }, +} + +export function extractPagination(payload: unknown): { + totalResults: number | null + currentPage: number | null + totalPages: number | null +} { + if (payload && typeof payload === 'object') { + const meta = (payload as Record).meta as + | { totalResults?: unknown; page?: { number?: unknown; total?: unknown } } + | undefined + if (meta) { + const totalResults = typeof meta.totalResults === 'number' ? meta.totalResults : null + const currentPage = + meta.page && typeof meta.page.number === 'number' ? meta.page.number : null + const totalPages = meta.page && typeof meta.page.total === 'number' ? meta.page.total : null + return { totalResults, currentPage, totalPages } + } + } + return { totalResults: null, currentPage: null, totalPages: null } +} + +export function extractDataArray(payload: unknown): Array> { + if (payload && typeof payload === 'object') { + const data = (payload as Record).data + if (Array.isArray(data)) return data as Array> + } + return [] +} diff --git a/scripts/check-api-validation-contracts.ts b/scripts/check-api-validation-contracts.ts index b3c105ab531..93b187d8aa6 100644 --- a/scripts/check-api-validation-contracts.ts +++ b/scripts/check-api-validation-contracts.ts @@ -9,8 +9,8 @@ const QUERY_HOOKS_DIR = path.join(ROOT, 'apps/sim/hooks/queries') const SELECTOR_HOOKS_DIR = path.join(ROOT, 'apps/sim/hooks/selectors') const BASELINE = { - totalRoutes: 756, - zodRoutes: 756, + totalRoutes: 757, + zodRoutes: 757, nonZodRoutes: 0, } as const