From 1fa904f12a5e88543774c56ff0085fda755d6db4 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 25 Feb 2026 12:07:44 -0800 Subject: [PATCH 1/5] feat(google-contacts): add google contacts integration --- apps/docs/components/icons.tsx | 26 ++ apps/docs/components/ui/icon-mapping.ts | 14 +- .../content/docs/en/tools/google_contacts.mdx | 144 ++++++++++ apps/docs/content/docs/en/tools/meta.json | 3 +- .../components/oauth-required-modal.tsx | 1 + apps/sim/blocks/blocks/google_contacts.ts | 265 ++++++++++++++++++ apps/sim/blocks/registry.ts | 2 + apps/sim/components/icons.tsx | 26 ++ apps/sim/lib/auth/auth.ts | 40 +++ apps/sim/lib/oauth/oauth.ts | 9 + apps/sim/lib/oauth/types.ts | 2 + apps/sim/tools/google_contacts/create.ts | 146 ++++++++++ apps/sim/tools/google_contacts/delete.ts | 69 +++++ apps/sim/tools/google_contacts/get.ts | 66 +++++ apps/sim/tools/google_contacts/index.ts | 13 + apps/sim/tools/google_contacts/list.ts | 91 ++++++ apps/sim/tools/google_contacts/search.ts | 84 ++++++ apps/sim/tools/google_contacts/types.ts | 181 ++++++++++++ apps/sim/tools/google_contacts/update.ts | 173 ++++++++++++ apps/sim/tools/registry.ts | 14 + 20 files changed, 1362 insertions(+), 7 deletions(-) create mode 100644 apps/docs/content/docs/en/tools/google_contacts.mdx create mode 100644 apps/sim/blocks/blocks/google_contacts.ts create mode 100644 apps/sim/tools/google_contacts/create.ts create mode 100644 apps/sim/tools/google_contacts/delete.ts create mode 100644 apps/sim/tools/google_contacts/get.ts create mode 100644 apps/sim/tools/google_contacts/index.ts create mode 100644 apps/sim/tools/google_contacts/list.ts create mode 100644 apps/sim/tools/google_contacts/search.ts create mode 100644 apps/sim/tools/google_contacts/types.ts create mode 100644 apps/sim/tools/google_contacts/update.ts diff --git a/apps/docs/components/icons.tsx b/apps/docs/components/icons.tsx index dcd5741f2b..3e08689b14 100644 --- a/apps/docs/components/icons.tsx +++ b/apps/docs/components/icons.tsx @@ -1235,6 +1235,32 @@ export function GoogleSlidesIcon(props: SVGProps) { ) } +export function GoogleContactsIcon(props: SVGProps) { + return ( + + + + + + + + ) +} + export function GoogleCalendarIcon(props: SVGProps) { return ( > @@ -189,6 +190,7 @@ export const blockTypeToIconMap: Record = { gong: GongIcon, google_books: GoogleBooksIcon, google_calendar_v2: GoogleCalendarIcon, + google_contacts: GoogleContactsIcon, google_docs: GoogleDocsIcon, google_drive: GoogleDriveIcon, google_forms: GoogleFormsIcon, diff --git a/apps/docs/content/docs/en/tools/google_contacts.mdx b/apps/docs/content/docs/en/tools/google_contacts.mdx new file mode 100644 index 0000000000..b68c303a57 --- /dev/null +++ b/apps/docs/content/docs/en/tools/google_contacts.mdx @@ -0,0 +1,144 @@ +--- +title: Google Contacts +description: Manage Google Contacts +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + +## Usage Instructions + +Integrate Google Contacts into the workflow. Can create, read, update, delete, list, and search contacts. + + + +## Tools + +### `google_contacts_create` + +Create a new contact in Google Contacts + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `givenName` | string | Yes | First name of the contact | +| `familyName` | string | No | Last name of the contact | +| `email` | string | No | Email address of the contact | +| `emailType` | string | No | Email type: home, work, or other | +| `phone` | string | No | Phone number of the contact | +| `phoneType` | string | No | Phone type: mobile, home, work, or other | +| `organization` | string | No | Organization/company name | +| `jobTitle` | string | No | Job title at the organization | +| `notes` | string | No | Notes or biography for the contact | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `content` | string | Contact creation confirmation message | +| `metadata` | json | Created contact metadata including resource name and details | + +### `google_contacts_get` + +Get a specific contact from Google Contacts + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `resourceName` | string | Yes | Resource name of the contact \(e.g., people/c1234567890\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `content` | string | Contact retrieval confirmation message | +| `metadata` | json | Contact details including name, email, phone, and organization | + +### `google_contacts_list` + +List contacts from Google Contacts + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `pageSize` | number | No | Number of contacts to return \(1-1000, default 100\) | +| `pageToken` | string | No | Page token from a previous list request for pagination | +| `sortOrder` | string | No | Sort order for contacts | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `content` | string | Summary of found contacts count | +| `metadata` | json | List of contacts with pagination tokens | + +### `google_contacts_search` + +Search contacts in Google Contacts by name, email, phone, or organization + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `query` | string | Yes | Search query to match against contact names, emails, phones, and organizations | +| `pageSize` | number | No | Number of results to return \(default 10, max 30\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `content` | string | Summary of search results count | +| `metadata` | json | Search results with matching contacts | + +### `google_contacts_update` + +Update an existing contact in Google Contacts + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `resourceName` | string | Yes | Resource name of the contact \(e.g., people/c1234567890\) | +| `etag` | string | Yes | ETag from a previous get request \(required for concurrency control\) | +| `givenName` | string | No | Updated first name | +| `familyName` | string | No | Updated last name | +| `email` | string | No | Updated email address | +| `emailType` | string | No | Email type: home, work, or other | +| `phone` | string | No | Updated phone number | +| `phoneType` | string | No | Phone type: mobile, home, work, or other | +| `organization` | string | No | Updated organization/company name | +| `jobTitle` | string | No | Updated job title | +| `notes` | string | No | Updated notes or biography | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `content` | string | Contact update confirmation message | +| `metadata` | json | Updated contact metadata | + +### `google_contacts_delete` + +Delete a contact from Google Contacts + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `resourceName` | string | Yes | Resource name of the contact to delete \(e.g., people/c1234567890\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `content` | string | Contact deletion confirmation message | +| `metadata` | json | Deletion details including resource name | + + diff --git a/apps/docs/content/docs/en/tools/meta.json b/apps/docs/content/docs/en/tools/meta.json index 9fc1cc577e..2bc389115f 100644 --- a/apps/docs/content/docs/en/tools/meta.json +++ b/apps/docs/content/docs/en/tools/meta.json @@ -39,6 +39,7 @@ "gong", "google_books", "google_calendar", + "google_contacts", "google_docs", "google_drive", "google_forms", @@ -146,4 +147,4 @@ "zep", "zoom" ] -} +} \ No newline at end of file diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/components/oauth-required-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/components/oauth-required-modal.tsx index 6cac32e626..f74cffa205 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/components/oauth-required-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/components/oauth-required-modal.tsx @@ -40,6 +40,7 @@ const SCOPE_DESCRIPTIONS: Record = { 'https://www.googleapis.com/auth/drive.file': 'View and manage Google Drive files', 'https://www.googleapis.com/auth/drive': 'Access all Google Drive files', 'https://www.googleapis.com/auth/calendar': 'View and manage calendar', + 'https://www.googleapis.com/auth/contacts': 'View and manage Google Contacts', 'https://www.googleapis.com/auth/userinfo.email': 'View email address', 'https://www.googleapis.com/auth/userinfo.profile': 'View basic profile info', 'https://www.googleapis.com/auth/forms.body': 'View and manage Google Forms', diff --git a/apps/sim/blocks/blocks/google_contacts.ts b/apps/sim/blocks/blocks/google_contacts.ts new file mode 100644 index 0000000000..8504187ba5 --- /dev/null +++ b/apps/sim/blocks/blocks/google_contacts.ts @@ -0,0 +1,265 @@ +import { GoogleContactsIcon } from '@/components/icons' +import type { BlockConfig } from '@/blocks/types' +import { AuthMode } from '@/blocks/types' +import type { GoogleContactsResponse } from '@/tools/google_contacts/types' + +export const GoogleContactsBlock: BlockConfig = { + type: 'google_contacts', + name: 'Google Contacts', + description: 'Manage Google Contacts', + authMode: AuthMode.OAuth, + longDescription: + 'Integrate Google Contacts into the workflow. Can create, read, update, delete, list, and search contacts.', + docsLink: 'https://docs.sim.ai/tools/google_contacts', + category: 'tools', + bgColor: '#E0E0E0', + icon: GoogleContactsIcon, + subBlocks: [ + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + options: [ + { label: 'Create Contact', id: 'create' }, + { label: 'Get Contact', id: 'get' }, + { label: 'List Contacts', id: 'list' }, + { label: 'Search Contacts', id: 'search' }, + { label: 'Update Contact', id: 'update' }, + { label: 'Delete Contact', id: 'delete' }, + ], + value: () => 'create', + }, + { + id: 'credential', + title: 'Google Contacts Account', + type: 'oauth-input', + canonicalParamId: 'oauthCredential', + mode: 'basic', + required: true, + serviceId: 'google-contacts', + requiredScopes: ['https://www.googleapis.com/auth/contacts'], + placeholder: 'Select Google account', + }, + { + id: 'manualCredential', + title: 'Google Contacts Account', + type: 'short-input', + canonicalParamId: 'oauthCredential', + mode: 'advanced', + placeholder: 'Enter credential ID', + required: true, + }, + + // Create Contact Fields + { + id: 'givenName', + title: 'First Name', + type: 'short-input', + placeholder: 'John', + condition: { field: 'operation', value: ['create', 'update'] }, + required: { field: 'operation', value: 'create' }, + }, + { + id: 'familyName', + title: 'Last Name', + type: 'short-input', + placeholder: 'Doe', + condition: { field: 'operation', value: ['create', 'update'] }, + }, + { + id: 'email', + title: 'Email', + type: 'short-input', + placeholder: 'john@example.com', + condition: { field: 'operation', value: ['create', 'update'] }, + }, + { + id: 'emailType', + title: 'Email Type', + type: 'dropdown', + condition: { field: 'operation', value: ['create', 'update'] }, + options: [ + { label: 'Work', id: 'work' }, + { label: 'Home', id: 'home' }, + { label: 'Other', id: 'other' }, + ], + value: () => 'work', + }, + { + id: 'phone', + title: 'Phone', + type: 'short-input', + placeholder: '+1234567890', + condition: { field: 'operation', value: ['create', 'update'] }, + }, + { + id: 'phoneType', + title: 'Phone Type', + type: 'dropdown', + condition: { field: 'operation', value: ['create', 'update'] }, + options: [ + { label: 'Mobile', id: 'mobile' }, + { label: 'Home', id: 'home' }, + { label: 'Work', id: 'work' }, + { label: 'Other', id: 'other' }, + ], + value: () => 'mobile', + }, + { + id: 'organization', + title: 'Organization', + type: 'short-input', + placeholder: 'Acme Corp', + condition: { field: 'operation', value: ['create', 'update'] }, + }, + { + id: 'jobTitle', + title: 'Job Title', + type: 'short-input', + placeholder: 'Software Engineer', + condition: { field: 'operation', value: ['create', 'update'] }, + }, + { + id: 'notes', + title: 'Notes', + type: 'long-input', + placeholder: 'Additional notes about the contact', + condition: { field: 'operation', value: ['create', 'update'] }, + }, + + // Get / Update / Delete Fields + { + id: 'resourceName', + title: 'Resource Name', + type: 'short-input', + placeholder: 'people/c1234567890', + condition: { field: 'operation', value: ['get', 'update', 'delete'] }, + required: { field: 'operation', value: ['get', 'update', 'delete'] }, + }, + + // Update requires etag + { + id: 'etag', + title: 'ETag', + type: 'short-input', + placeholder: 'ETag from a previous get request', + condition: { field: 'operation', value: 'update' }, + required: { field: 'operation', value: 'update' }, + }, + + // Search Fields + { + id: 'query', + title: 'Search Query', + type: 'short-input', + placeholder: 'Search by name, email, phone, or organization', + condition: { field: 'operation', value: 'search' }, + required: { field: 'operation', value: 'search' }, + }, + + // List/Search Fields + { + id: 'pageSize', + title: 'Page Size', + type: 'short-input', + placeholder: '100', + condition: { field: 'operation', value: ['list', 'search'] }, + }, + { + id: 'pageToken', + title: 'Page Token', + type: 'short-input', + placeholder: 'Token from previous list request', + condition: { field: 'operation', value: 'list' }, + }, + { + id: 'sortOrder', + title: 'Sort Order', + type: 'dropdown', + condition: { field: 'operation', value: 'list' }, + options: [ + { label: 'Last Modified (Descending)', id: 'LAST_MODIFIED_DESCENDING' }, + { label: 'Last Modified (Ascending)', id: 'LAST_MODIFIED_ASCENDING' }, + { label: 'First Name (Ascending)', id: 'FIRST_NAME_ASCENDING' }, + { label: 'Last Name (Ascending)', id: 'LAST_NAME_ASCENDING' }, + ], + value: () => 'LAST_MODIFIED_DESCENDING', + }, + ], + tools: { + access: [ + 'google_contacts_create', + 'google_contacts_get', + 'google_contacts_list', + 'google_contacts_search', + 'google_contacts_update', + 'google_contacts_delete', + ], + config: { + tool: (params) => { + switch (params.operation) { + case 'create': + return 'google_contacts_create' + case 'get': + return 'google_contacts_get' + case 'list': + return 'google_contacts_list' + case 'search': + return 'google_contacts_search' + case 'update': + return 'google_contacts_update' + case 'delete': + return 'google_contacts_delete' + default: + throw new Error(`Invalid Google Contacts operation: ${params.operation}`) + } + }, + params: (params) => { + const { oauthCredential, operation, ...rest } = params + + const processedParams: Record = { ...rest } + + // Convert pageSize to number if provided + if (processedParams.pageSize && typeof processedParams.pageSize === 'string') { + processedParams.pageSize = Number.parseInt(processedParams.pageSize, 10) + } + + return { + oauthCredential, + ...processedParams, + } + }, + }, + }, + inputs: { + operation: { type: 'string', description: 'Operation to perform' }, + oauthCredential: { type: 'string', description: 'Google Contacts access token' }, + + // Create/Update inputs + givenName: { type: 'string', description: 'First name' }, + familyName: { type: 'string', description: 'Last name' }, + email: { type: 'string', description: 'Email address' }, + emailType: { type: 'string', description: 'Email type' }, + phone: { type: 'string', description: 'Phone number' }, + phoneType: { type: 'string', description: 'Phone type' }, + organization: { type: 'string', description: 'Organization name' }, + jobTitle: { type: 'string', description: 'Job title' }, + notes: { type: 'string', description: 'Notes' }, + + // Get/Update/Delete inputs + resourceName: { type: 'string', description: 'Contact resource name' }, + etag: { type: 'string', description: 'Contact ETag for updates' }, + + // Search inputs + query: { type: 'string', description: 'Search query' }, + + // List inputs + pageSize: { type: 'string', description: 'Number of results' }, + pageToken: { type: 'string', description: 'Pagination token' }, + sortOrder: { type: 'string', description: 'Sort order' }, + }, + outputs: { + content: { type: 'string', description: 'Operation response content' }, + metadata: { type: 'json', description: 'Contact or contacts metadata' }, + }, +} diff --git a/apps/sim/blocks/registry.ts b/apps/sim/blocks/registry.ts index 03b9827a77..20b932892c 100644 --- a/apps/sim/blocks/registry.ts +++ b/apps/sim/blocks/registry.ts @@ -45,6 +45,7 @@ import { GongBlock } from '@/blocks/blocks/gong' import { GoogleSearchBlock } from '@/blocks/blocks/google' import { GoogleBooksBlock } from '@/blocks/blocks/google_books' import { GoogleCalendarBlock, GoogleCalendarV2Block } from '@/blocks/blocks/google_calendar' +import { GoogleContactsBlock } from '@/blocks/blocks/google_contacts' import { GoogleDocsBlock } from '@/blocks/blocks/google_docs' import { GoogleDriveBlock } from '@/blocks/blocks/google_drive' import { GoogleFormsBlock } from '@/blocks/blocks/google_forms' @@ -229,6 +230,7 @@ export const registry: Record = { google_calendar: GoogleCalendarBlock, google_calendar_v2: GoogleCalendarV2Block, google_books: GoogleBooksBlock, + google_contacts: GoogleContactsBlock, google_docs: GoogleDocsBlock, google_drive: GoogleDriveBlock, google_forms: GoogleFormsBlock, diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index dcd5741f2b..3e08689b14 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -1235,6 +1235,32 @@ export function GoogleSlidesIcon(props: SVGProps) { ) } +export function GoogleContactsIcon(props: SVGProps) { + return ( + + + + + + + + ) +} + export function GoogleCalendarIcon(props: SVGProps) { return ( { + try { + const response = await fetch('https://openidconnect.googleapis.com/v1/userinfo', { + headers: { Authorization: `Bearer ${tokens.accessToken}` }, + }) + if (!response.ok) { + logger.error('Failed to fetch Google user info', { status: response.status }) + throw new Error(`Failed to fetch Google user info: ${response.statusText}`) + } + const profile = await response.json() + const now = new Date() + return { + id: `${profile.sub}-${crypto.randomUUID()}`, + name: profile.name || 'Google User', + email: profile.email, + image: profile.picture || undefined, + emailVerified: profile.email_verified || false, + createdAt: now, + updatedAt: now, + } + } catch (error) { + logger.error('Error in Google getUserInfo', { error }) + throw error + } + }, + }, { providerId: 'google-forms', clientId: env.GOOGLE_CLIENT_ID as string, diff --git a/apps/sim/lib/oauth/oauth.ts b/apps/sim/lib/oauth/oauth.ts index b890566334..c9cfa07307 100644 --- a/apps/sim/lib/oauth/oauth.ts +++ b/apps/sim/lib/oauth/oauth.ts @@ -9,6 +9,7 @@ import { GithubIcon, GmailIcon, GoogleCalendarIcon, + GoogleContactsIcon, GoogleDocsIcon, GoogleDriveIcon, GoogleFormsIcon, @@ -119,6 +120,14 @@ export const OAUTH_PROVIDERS: Record = { baseProviderIcon: GoogleIcon, scopes: ['https://www.googleapis.com/auth/calendar'], }, + 'google-contacts': { + name: 'Google Contacts', + description: 'Create, read, update, and search contacts with Google Contacts.', + providerId: 'google-contacts', + icon: GoogleContactsIcon, + baseProviderIcon: GoogleIcon, + scopes: ['https://www.googleapis.com/auth/contacts'], + }, 'google-vault': { name: 'Google Vault', description: 'Search, export, and manage matters/holds via Google Vault.', diff --git a/apps/sim/lib/oauth/types.ts b/apps/sim/lib/oauth/types.ts index d5114a38bc..2a988671f6 100644 --- a/apps/sim/lib/oauth/types.ts +++ b/apps/sim/lib/oauth/types.ts @@ -7,6 +7,7 @@ export type OAuthProvider = | 'google-docs' | 'google-sheets' | 'google-calendar' + | 'google-contacts' | 'google-vault' | 'google-forms' | 'google-groups' @@ -52,6 +53,7 @@ export type OAuthService = | 'google-docs' | 'google-sheets' | 'google-calendar' + | 'google-contacts' | 'google-vault' | 'google-forms' | 'google-groups' diff --git a/apps/sim/tools/google_contacts/create.ts b/apps/sim/tools/google_contacts/create.ts new file mode 100644 index 0000000000..939fc739a0 --- /dev/null +++ b/apps/sim/tools/google_contacts/create.ts @@ -0,0 +1,146 @@ +import { + DEFAULT_PERSON_FIELDS, + type GoogleContactsCreateParams, + type GoogleContactsCreateResponse, + PEOPLE_API_BASE, + transformPerson, +} from '@/tools/google_contacts/types' +import type { ToolConfig } from '@/tools/types' + +export const createTool: ToolConfig = { + id: 'google_contacts_create', + name: 'Google Contacts Create', + description: 'Create a new contact in Google Contacts', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-contacts', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Access token for Google People API', + }, + givenName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'First name of the contact', + }, + familyName: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Last name of the contact', + }, + email: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Email address of the contact', + }, + emailType: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Email type: home, work, or other', + }, + phone: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Phone number of the contact', + }, + phoneType: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Phone type: mobile, home, work, or other', + }, + organization: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Organization/company name', + }, + jobTitle: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Job title at the organization', + }, + notes: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Notes or biography for the contact', + }, + }, + + request: { + url: () => `${PEOPLE_API_BASE}/people:createContact?personFields=${DEFAULT_PERSON_FIELDS}`, + method: 'POST', + headers: (params: GoogleContactsCreateParams) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + body: (params: GoogleContactsCreateParams) => { + const person: Record = { + names: [ + { + givenName: params.givenName, + ...(params.familyName ? { familyName: params.familyName } : {}), + }, + ], + } + + if (params.email) { + person.emailAddresses = [{ value: params.email, type: params.emailType || 'other' }] + } + + if (params.phone) { + person.phoneNumbers = [{ value: params.phone, type: params.phoneType || 'mobile' }] + } + + if (params.organization || params.jobTitle) { + person.organizations = [ + { + ...(params.organization ? { name: params.organization } : {}), + ...(params.jobTitle ? { title: params.jobTitle } : {}), + }, + ] + } + + if (params.notes) { + person.biographies = [{ value: params.notes, contentType: 'TEXT_PLAIN' }] + } + + return person + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + const contact = transformPerson(data) + + return { + success: true, + output: { + content: `Contact "${contact.displayName || contact.givenName}" created successfully`, + metadata: contact, + }, + } + }, + + outputs: { + content: { type: 'string', description: 'Contact creation confirmation message' }, + metadata: { + type: 'json', + description: 'Created contact metadata including resource name and details', + }, + }, +} diff --git a/apps/sim/tools/google_contacts/delete.ts b/apps/sim/tools/google_contacts/delete.ts new file mode 100644 index 0000000000..ab4bcaf935 --- /dev/null +++ b/apps/sim/tools/google_contacts/delete.ts @@ -0,0 +1,69 @@ +import { + type GoogleContactsDeleteParams, + type GoogleContactsDeleteResponse, + PEOPLE_API_BASE, +} from '@/tools/google_contacts/types' +import type { ToolConfig } from '@/tools/types' + +export const deleteTool: ToolConfig = { + id: 'google_contacts_delete', + name: 'Google Contacts Delete', + description: 'Delete a contact from Google Contacts', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-contacts', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Access token for Google People API', + }, + resourceName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Resource name of the contact to delete (e.g., people/c1234567890)', + }, + }, + + request: { + url: (params: GoogleContactsDeleteParams) => + `${PEOPLE_API_BASE}/${params.resourceName}:deleteContact`, + method: 'DELETE', + headers: (params: GoogleContactsDeleteParams) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response, params) => { + if (response.status === 200 || response.status === 204 || response.ok) { + return { + success: true, + output: { + content: 'Contact successfully deleted', + metadata: { + resourceName: params?.resourceName || '', + deleted: true, + }, + }, + } + } + + const errorData = await response.json() + throw new Error(errorData.error?.message || 'Failed to delete contact') + }, + + outputs: { + content: { type: 'string', description: 'Contact deletion confirmation message' }, + metadata: { + type: 'json', + description: 'Deletion details including resource name', + }, + }, +} diff --git a/apps/sim/tools/google_contacts/get.ts b/apps/sim/tools/google_contacts/get.ts new file mode 100644 index 0000000000..ae44178ad2 --- /dev/null +++ b/apps/sim/tools/google_contacts/get.ts @@ -0,0 +1,66 @@ +import { + DEFAULT_PERSON_FIELDS, + type GoogleContactsGetParams, + type GoogleContactsGetResponse, + PEOPLE_API_BASE, + transformPerson, +} from '@/tools/google_contacts/types' +import type { ToolConfig } from '@/tools/types' + +export const getTool: ToolConfig = { + id: 'google_contacts_get', + name: 'Google Contacts Get', + description: 'Get a specific contact from Google Contacts', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-contacts', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Access token for Google People API', + }, + resourceName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Resource name of the contact (e.g., people/c1234567890)', + }, + }, + + request: { + url: (params: GoogleContactsGetParams) => + `${PEOPLE_API_BASE}/${params.resourceName}?personFields=${DEFAULT_PERSON_FIELDS}`, + method: 'GET', + headers: (params: GoogleContactsGetParams) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + const contact = transformPerson(data) + + return { + success: true, + output: { + content: `Retrieved contact "${contact.displayName || contact.resourceName}"`, + metadata: contact, + }, + } + }, + + outputs: { + content: { type: 'string', description: 'Contact retrieval confirmation message' }, + metadata: { + type: 'json', + description: 'Contact details including name, email, phone, and organization', + }, + }, +} diff --git a/apps/sim/tools/google_contacts/index.ts b/apps/sim/tools/google_contacts/index.ts new file mode 100644 index 0000000000..1eb3c84669 --- /dev/null +++ b/apps/sim/tools/google_contacts/index.ts @@ -0,0 +1,13 @@ +import { createTool } from '@/tools/google_contacts/create' +import { deleteTool } from '@/tools/google_contacts/delete' +import { getTool } from '@/tools/google_contacts/get' +import { listTool } from '@/tools/google_contacts/list' +import { searchTool } from '@/tools/google_contacts/search' +import { updateTool } from '@/tools/google_contacts/update' + +export const googleContactsCreateTool = createTool +export const googleContactsDeleteTool = deleteTool +export const googleContactsGetTool = getTool +export const googleContactsListTool = listTool +export const googleContactsSearchTool = searchTool +export const googleContactsUpdateTool = updateTool diff --git a/apps/sim/tools/google_contacts/list.ts b/apps/sim/tools/google_contacts/list.ts new file mode 100644 index 0000000000..05b32e80e0 --- /dev/null +++ b/apps/sim/tools/google_contacts/list.ts @@ -0,0 +1,91 @@ +import { + DEFAULT_PERSON_FIELDS, + type GoogleContactsListParams, + type GoogleContactsListResponse, + PEOPLE_API_BASE, + transformPerson, +} from '@/tools/google_contacts/types' +import type { ToolConfig } from '@/tools/types' + +export const listTool: ToolConfig = { + id: 'google_contacts_list', + name: 'Google Contacts List', + description: 'List contacts from Google Contacts', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-contacts', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Access token for Google People API', + }, + pageSize: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of contacts to return (1-1000, default 100)', + }, + pageToken: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page token from a previous list request for pagination', + }, + sortOrder: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Sort order for contacts', + }, + }, + + request: { + url: (params: GoogleContactsListParams) => { + const queryParams = new URLSearchParams() + queryParams.append('personFields', DEFAULT_PERSON_FIELDS) + + if (params.pageSize) queryParams.append('pageSize', params.pageSize.toString()) + if (params.pageToken) queryParams.append('pageToken', params.pageToken) + if (params.sortOrder) queryParams.append('sortOrder', params.sortOrder) + + return `${PEOPLE_API_BASE}/people/me/connections?${queryParams.toString()}` + }, + method: 'GET', + headers: (params: GoogleContactsListParams) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + const connections = data.connections || [] + const contacts = connections.map((person: Record) => transformPerson(person)) + + return { + success: true, + output: { + content: `Found ${contacts.length} contact${contacts.length !== 1 ? 's' : ''}`, + metadata: { + totalItems: data.totalItems ?? null, + nextPageToken: data.nextPageToken ?? null, + contacts, + }, + }, + } + }, + + outputs: { + content: { type: 'string', description: 'Summary of found contacts count' }, + metadata: { + type: 'json', + description: 'List of contacts with pagination tokens', + }, + }, +} diff --git a/apps/sim/tools/google_contacts/search.ts b/apps/sim/tools/google_contacts/search.ts new file mode 100644 index 0000000000..226426c3e2 --- /dev/null +++ b/apps/sim/tools/google_contacts/search.ts @@ -0,0 +1,84 @@ +import { + DEFAULT_PERSON_FIELDS, + type GoogleContactsSearchParams, + type GoogleContactsSearchResponse, + PEOPLE_API_BASE, + transformPerson, +} from '@/tools/google_contacts/types' +import type { ToolConfig } from '@/tools/types' + +export const searchTool: ToolConfig = { + id: 'google_contacts_search', + name: 'Google Contacts Search', + description: 'Search contacts in Google Contacts by name, email, phone, or organization', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-contacts', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Access token for Google People API', + }, + query: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Search query to match against contact names, emails, phones, and organizations', + }, + pageSize: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of results to return (default 10, max 30)', + }, + }, + + request: { + url: (params: GoogleContactsSearchParams) => { + const queryParams = new URLSearchParams() + queryParams.append('query', params.query) + queryParams.append('readMask', DEFAULT_PERSON_FIELDS) + + if (params.pageSize) queryParams.append('pageSize', params.pageSize.toString()) + + return `${PEOPLE_API_BASE}/people:searchContacts?${queryParams.toString()}` + }, + method: 'GET', + headers: (params: GoogleContactsSearchParams) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + const results = data.results || [] + const contacts = results.map((result: Record) => + transformPerson(result.person || result) + ) + + return { + success: true, + output: { + content: `Found ${contacts.length} contact${contacts.length !== 1 ? 's' : ''} matching query`, + metadata: { + contacts, + }, + }, + } + }, + + outputs: { + content: { type: 'string', description: 'Summary of search results count' }, + metadata: { + type: 'json', + description: 'Search results with matching contacts', + }, + }, +} diff --git a/apps/sim/tools/google_contacts/types.ts b/apps/sim/tools/google_contacts/types.ts new file mode 100644 index 0000000000..c17c76f041 --- /dev/null +++ b/apps/sim/tools/google_contacts/types.ts @@ -0,0 +1,181 @@ +import type { ToolResponse } from '@/tools/types' + +export const PEOPLE_API_BASE = 'https://people.googleapis.com/v1' + +export const DEFAULT_PERSON_FIELDS = + 'names,emailAddresses,phoneNumbers,organizations,addresses,biographies,urls,photos,metadata' + +interface BaseGoogleContactsParams { + accessToken: string +} + +export interface GoogleContactsCreateParams extends BaseGoogleContactsParams { + givenName: string + familyName?: string + email?: string + emailType?: 'home' | 'work' | 'other' + phone?: string + phoneType?: 'mobile' | 'home' | 'work' | 'other' + organization?: string + jobTitle?: string + notes?: string +} + +export interface GoogleContactsGetParams extends BaseGoogleContactsParams { + resourceName: string +} + +export interface GoogleContactsListParams extends BaseGoogleContactsParams { + pageSize?: number + pageToken?: string + sortOrder?: 'LAST_MODIFIED_ASCENDING' | 'LAST_MODIFIED_DESCENDING' | 'FIRST_NAME_ASCENDING' | 'LAST_NAME_ASCENDING' +} + +export interface GoogleContactsUpdateParams extends BaseGoogleContactsParams { + resourceName: string + etag: string + givenName?: string + familyName?: string + email?: string + emailType?: 'home' | 'work' | 'other' + phone?: string + phoneType?: 'mobile' | 'home' | 'work' | 'other' + organization?: string + jobTitle?: string + notes?: string +} + +export interface GoogleContactsDeleteParams extends BaseGoogleContactsParams { + resourceName: string +} + +export interface GoogleContactsSearchParams extends BaseGoogleContactsParams { + query: string + pageSize?: number +} + +export type GoogleContactsToolParams = + | GoogleContactsCreateParams + | GoogleContactsGetParams + | GoogleContactsListParams + | GoogleContactsUpdateParams + | GoogleContactsDeleteParams + | GoogleContactsSearchParams + +interface ContactMetadata { + resourceName: string + etag: string + displayName: string | null + givenName: string | null + familyName: string | null + emails: Array<{ value: string; type: string }> | null + phones: Array<{ value: string; type: string }> | null + organizations: Array<{ name: string; title: string }> | null + addresses: Array<{ formattedValue: string; type: string }> | null + biographies: Array<{ value: string }> | null + urls: Array<{ value: string; type: string }> | null + photos: Array<{ url: string }> | null +} + +export interface GoogleContactsCreateResponse extends ToolResponse { + output: { + content: string + metadata: ContactMetadata + } +} + +export interface GoogleContactsGetResponse extends ToolResponse { + output: { + content: string + metadata: ContactMetadata + } +} + +export interface GoogleContactsListResponse extends ToolResponse { + output: { + content: string + metadata: { + totalItems: number | null + nextPageToken: string | null + contacts: ContactMetadata[] + } + } +} + +export interface GoogleContactsUpdateResponse extends ToolResponse { + output: { + content: string + metadata: ContactMetadata + } +} + +export interface GoogleContactsDeleteResponse extends ToolResponse { + output: { + content: string + metadata: { + resourceName: string + deleted: boolean + } + } +} + +export interface GoogleContactsSearchResponse extends ToolResponse { + output: { + content: string + metadata: { + contacts: ContactMetadata[] + } + } +} + +export type GoogleContactsResponse = + | GoogleContactsCreateResponse + | GoogleContactsGetResponse + | GoogleContactsListResponse + | GoogleContactsUpdateResponse + | GoogleContactsDeleteResponse + | GoogleContactsSearchResponse + +/** Transforms a raw Google People API person object into a ContactMetadata */ +export function transformPerson(person: Record): ContactMetadata { + return { + resourceName: person.resourceName ?? '', + etag: person.etag ?? '', + displayName: person.names?.[0]?.displayName ?? null, + givenName: person.names?.[0]?.givenName ?? null, + familyName: person.names?.[0]?.familyName ?? null, + emails: + person.emailAddresses?.map((e: Record) => ({ + value: e.value ?? '', + type: e.type ?? 'other', + })) ?? null, + phones: + person.phoneNumbers?.map((p: Record) => ({ + value: p.value ?? '', + type: p.type ?? 'other', + })) ?? null, + organizations: + person.organizations?.map((o: Record) => ({ + name: o.name ?? '', + title: o.title ?? '', + })) ?? null, + addresses: + person.addresses?.map((a: Record) => ({ + formattedValue: a.formattedValue ?? '', + type: a.type ?? 'other', + })) ?? null, + biographies: + person.biographies?.map((b: Record) => ({ + value: b.value ?? '', + })) ?? null, + urls: + person.urls?.map((u: Record) => ({ + value: u.value ?? '', + type: u.type ?? 'other', + })) ?? null, + photos: + person.photos?.map((p: Record) => ({ + url: p.url ?? '', + })) ?? null, + } +} diff --git a/apps/sim/tools/google_contacts/update.ts b/apps/sim/tools/google_contacts/update.ts new file mode 100644 index 0000000000..1e96eabdba --- /dev/null +++ b/apps/sim/tools/google_contacts/update.ts @@ -0,0 +1,173 @@ +import { + DEFAULT_PERSON_FIELDS, + type GoogleContactsUpdateParams, + type GoogleContactsUpdateResponse, + PEOPLE_API_BASE, + transformPerson, +} from '@/tools/google_contacts/types' +import type { ToolConfig } from '@/tools/types' + +export const updateTool: ToolConfig = { + id: 'google_contacts_update', + name: 'Google Contacts Update', + description: 'Update an existing contact in Google Contacts', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-contacts', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Access token for Google People API', + }, + resourceName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Resource name of the contact (e.g., people/c1234567890)', + }, + etag: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ETag from a previous get request (required for concurrency control)', + }, + givenName: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Updated first name', + }, + familyName: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Updated last name', + }, + email: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Updated email address', + }, + emailType: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Email type: home, work, or other', + }, + phone: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Updated phone number', + }, + phoneType: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Phone type: mobile, home, work, or other', + }, + organization: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Updated organization/company name', + }, + jobTitle: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Updated job title', + }, + notes: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Updated notes or biography', + }, + }, + + request: { + url: (params: GoogleContactsUpdateParams) => { + const updateFields: string[] = [] + if (params.givenName || params.familyName) updateFields.push('names') + if (params.email) updateFields.push('emailAddresses') + if (params.phone) updateFields.push('phoneNumbers') + if (params.organization || params.jobTitle) updateFields.push('organizations') + if (params.notes) updateFields.push('biographies') + + const updatePersonFields = updateFields.length > 0 ? updateFields.join(',') : 'names' + + return `${PEOPLE_API_BASE}/${params.resourceName}:updateContact?updatePersonFields=${updatePersonFields}&personFields=${DEFAULT_PERSON_FIELDS}` + }, + method: 'PATCH', + headers: (params: GoogleContactsUpdateParams) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + body: (params: GoogleContactsUpdateParams) => { + const person: Record = { + etag: params.etag, + } + + if (params.givenName || params.familyName) { + person.names = [ + { + ...(params.givenName ? { givenName: params.givenName } : {}), + ...(params.familyName ? { familyName: params.familyName } : {}), + }, + ] + } + + if (params.email) { + person.emailAddresses = [{ value: params.email, type: params.emailType || 'other' }] + } + + if (params.phone) { + person.phoneNumbers = [{ value: params.phone, type: params.phoneType || 'mobile' }] + } + + if (params.organization || params.jobTitle) { + person.organizations = [ + { + ...(params.organization ? { name: params.organization } : {}), + ...(params.jobTitle ? { title: params.jobTitle } : {}), + }, + ] + } + + if (params.notes) { + person.biographies = [{ value: params.notes, contentType: 'TEXT_PLAIN' }] + } + + return person + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + const contact = transformPerson(data) + + return { + success: true, + output: { + content: `Contact "${contact.displayName || contact.resourceName}" updated successfully`, + metadata: contact, + }, + } + }, + + outputs: { + content: { type: 'string', description: 'Contact update confirmation message' }, + metadata: { + type: 'json', + description: 'Updated contact metadata', + }, + }, +} diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 5a2f5787c7..9d85c58c38 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -643,6 +643,14 @@ import { googleCalendarUpdateTool, googleCalendarUpdateV2Tool, } from '@/tools/google_calendar' +import { + googleContactsCreateTool, + googleContactsDeleteTool, + googleContactsGetTool, + googleContactsListTool, + googleContactsSearchTool, + googleContactsUpdateTool, +} from '@/tools/google_contacts' import { googleDocsCreateTool, googleDocsReadTool, googleDocsWriteTool } from '@/tools/google_docs' import { googleDriveCopyTool, @@ -3505,6 +3513,12 @@ export const tools: Record = { google_calendar_quick_add_v2: googleCalendarQuickAddV2Tool, google_calendar_update: googleCalendarUpdateTool, google_calendar_update_v2: googleCalendarUpdateV2Tool, + google_contacts_create: googleContactsCreateTool, + google_contacts_delete: googleContactsDeleteTool, + google_contacts_get: googleContactsGetTool, + google_contacts_list: googleContactsListTool, + google_contacts_search: googleContactsSearchTool, + google_contacts_update: googleContactsUpdateTool, google_forms_get_responses: googleFormsGetResponsesTool, google_forms_get_form: googleFormsGetFormTool, google_forms_create_form: googleFormsCreateFormTool, From cfa321188a6b6e8c5aac30ffa6ff26bd31f05bea Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 25 Feb 2026 13:05:24 -0800 Subject: [PATCH 2/5] fix(google-contacts): throw error when no update fields provided --- apps/sim/tools/google_contacts/update.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/sim/tools/google_contacts/update.ts b/apps/sim/tools/google_contacts/update.ts index 1e96eabdba..33fed59f50 100644 --- a/apps/sim/tools/google_contacts/update.ts +++ b/apps/sim/tools/google_contacts/update.ts @@ -102,7 +102,11 @@ export const updateTool: ToolConfig 0 ? updateFields.join(',') : 'names' + if (updateFields.length === 0) { + throw new Error('At least one field to update must be provided') + } + + const updatePersonFields = updateFields.join(',') return `${PEOPLE_API_BASE}/${params.resourceName}:updateContact?updatePersonFields=${updatePersonFields}&personFields=${DEFAULT_PERSON_FIELDS}` }, From 49a6ff2f606397586c4dcaa9bb555650c69e561c Mon Sep 17 00:00:00 2001 From: waleed Date: Wed, 25 Feb 2026 13:27:23 -0800 Subject: [PATCH 3/5] lint --- apps/docs/components/icons.tsx | 7 +------ apps/docs/components/ui/icon-mapping.ts | 12 ++++++------ apps/docs/content/docs/en/tools/meta.json | 2 +- apps/sim/components/icons.tsx | 7 +------ apps/sim/tools/google_contacts/types.ts | 6 +++++- 5 files changed, 14 insertions(+), 20 deletions(-) diff --git a/apps/docs/components/icons.tsx b/apps/docs/components/icons.tsx index 3e08689b14..ee1c9d5481 100644 --- a/apps/docs/components/icons.tsx +++ b/apps/docs/components/icons.tsx @@ -1237,12 +1237,7 @@ export function GoogleSlidesIcon(props: SVGProps) { export function GoogleContactsIcon(props: SVGProps) { return ( - + > diff --git a/apps/docs/content/docs/en/tools/meta.json b/apps/docs/content/docs/en/tools/meta.json index 2bc389115f..420f79503a 100644 --- a/apps/docs/content/docs/en/tools/meta.json +++ b/apps/docs/content/docs/en/tools/meta.json @@ -147,4 +147,4 @@ "zep", "zoom" ] -} \ No newline at end of file +} diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index 3e08689b14..ee1c9d5481 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -1237,12 +1237,7 @@ export function GoogleSlidesIcon(props: SVGProps) { export function GoogleContactsIcon(props: SVGProps) { return ( - + Date: Fri, 27 Feb 2026 10:35:36 -0800 Subject: [PATCH 4/5] update icon --- apps/docs/components/icons.tsx | 19 ++++++------------- .../docs/content/docs/en/tools/databricks.mdx | 3 ++- .../docs/content/docs/en/tools/greenhouse.mdx | 3 ++- apps/sim/components/icons.tsx | 19 ++++++------------- apps/sim/lib/oauth/oauth.ts | 1 + 5 files changed, 17 insertions(+), 28 deletions(-) diff --git a/apps/docs/components/icons.tsx b/apps/docs/components/icons.tsx index 72f4880890..900ee4e13b 100644 --- a/apps/docs/components/icons.tsx +++ b/apps/docs/components/icons.tsx @@ -1267,21 +1267,14 @@ export function GoogleSlidesIcon(props: SVGProps) { export function GoogleContactsIcon(props: SVGProps) { return ( - - - - + + + - + ) } diff --git a/apps/docs/content/docs/en/tools/databricks.mdx b/apps/docs/content/docs/en/tools/databricks.mdx index c2dd7ddcf7..7286685fdc 100644 --- a/apps/docs/content/docs/en/tools/databricks.mdx +++ b/apps/docs/content/docs/en/tools/databricks.mdx @@ -5,7 +5,7 @@ description: Run SQL queries and manage jobs on Databricks import { BlockInfoCard } from "@/components/ui/block-info-card" - @@ -24,6 +24,7 @@ With the Databricks integration in Sim, you can: In Sim, the Databricks integration enables your agents to interact with your data lakehouse as part of automated workflows. Agents can query large-scale datasets, orchestrate ETL pipelines by triggering jobs, monitor job execution, and retrieve results—all without leaving the workflow canvas. This is ideal for automated reporting, data pipeline management, scheduled analytics, and building AI-driven data workflows that react to query results or job outcomes. {/* MANUAL-CONTENT-END */} + ## Usage Instructions Connect to Databricks to execute SQL queries against SQL warehouses, trigger and monitor job runs, manage clusters, and retrieve run outputs. Requires a Personal Access Token and workspace host URL. diff --git a/apps/docs/content/docs/en/tools/greenhouse.mdx b/apps/docs/content/docs/en/tools/greenhouse.mdx index 854fa86ac0..61e712935f 100644 --- a/apps/docs/content/docs/en/tools/greenhouse.mdx +++ b/apps/docs/content/docs/en/tools/greenhouse.mdx @@ -5,7 +5,7 @@ description: Manage candidates, jobs, and applications in Greenhouse import { BlockInfoCard } from "@/components/ui/block-info-card" - @@ -24,6 +24,7 @@ With the Greenhouse integration in Sim, you can: In Sim, the Greenhouse integration enables your agents to interact with your recruiting data as part of automated workflows. Agents can pull candidate information, monitor application pipelines, track job openings, and cross-reference hiring team data—all programmatically. This is ideal for building automated recruiting reports, candidate pipeline monitoring, hiring analytics dashboards, and workflows that react to changes in your talent pipeline. {/* MANUAL-CONTENT-END */} + ## Usage Instructions Integrate Greenhouse into the workflow. List and retrieve candidates, jobs, applications, users, departments, offices, and job stages from your Greenhouse ATS account. diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index 72f4880890..900ee4e13b 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -1267,21 +1267,14 @@ export function GoogleSlidesIcon(props: SVGProps) { export function GoogleContactsIcon(props: SVGProps) { return ( - - - - + + + - + ) } diff --git a/apps/sim/lib/oauth/oauth.ts b/apps/sim/lib/oauth/oauth.ts index f0290902cd..e1d3f862aa 100644 --- a/apps/sim/lib/oauth/oauth.ts +++ b/apps/sim/lib/oauth/oauth.ts @@ -129,6 +129,7 @@ export const OAUTH_PROVIDERS: Record = { icon: GoogleContactsIcon, baseProviderIcon: GoogleIcon, scopes: ['https://www.googleapis.com/auth/contacts'], + }, 'google-bigquery': { name: 'Google BigQuery', description: 'Query, list, and insert data in Google BigQuery.', From 8b676a51442d779d87dc886949351875f82fde93 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 27 Feb 2026 10:45:51 -0800 Subject: [PATCH 5/5] improvement(google-contacts): add advanced mode, error handling, and input trimming - Set mode: 'advanced' on optional fields (emailType, phoneType, notes, pageSize, pageToken, sortOrder) - Add createLogger and response.ok error handling to all 6 tools - Add .trim() on resourceName in get, update, delete URL builders --- apps/sim/blocks/blocks/google_contacts.ts | 6 ++++++ apps/sim/tools/google_contacts/create.ts | 10 ++++++++++ apps/sim/tools/google_contacts/delete.ts | 9 +++++++-- apps/sim/tools/google_contacts/get.ts | 12 +++++++++++- apps/sim/tools/google_contacts/list.ts | 10 ++++++++++ apps/sim/tools/google_contacts/search.ts | 10 ++++++++++ apps/sim/tools/google_contacts/update.ts | 12 +++++++++++- 7 files changed, 65 insertions(+), 4 deletions(-) diff --git a/apps/sim/blocks/blocks/google_contacts.ts b/apps/sim/blocks/blocks/google_contacts.ts index 8504187ba5..c2eb006df0 100644 --- a/apps/sim/blocks/blocks/google_contacts.ts +++ b/apps/sim/blocks/blocks/google_contacts.ts @@ -84,6 +84,7 @@ export const GoogleContactsBlock: BlockConfig = { { label: 'Other', id: 'other' }, ], value: () => 'work', + mode: 'advanced', }, { id: 'phone', @@ -104,6 +105,7 @@ export const GoogleContactsBlock: BlockConfig = { { label: 'Other', id: 'other' }, ], value: () => 'mobile', + mode: 'advanced', }, { id: 'organization', @@ -125,6 +127,7 @@ export const GoogleContactsBlock: BlockConfig = { type: 'long-input', placeholder: 'Additional notes about the contact', condition: { field: 'operation', value: ['create', 'update'] }, + mode: 'advanced', }, // Get / Update / Delete Fields @@ -164,6 +167,7 @@ export const GoogleContactsBlock: BlockConfig = { type: 'short-input', placeholder: '100', condition: { field: 'operation', value: ['list', 'search'] }, + mode: 'advanced', }, { id: 'pageToken', @@ -171,6 +175,7 @@ export const GoogleContactsBlock: BlockConfig = { type: 'short-input', placeholder: 'Token from previous list request', condition: { field: 'operation', value: 'list' }, + mode: 'advanced', }, { id: 'sortOrder', @@ -184,6 +189,7 @@ export const GoogleContactsBlock: BlockConfig = { { label: 'Last Name (Ascending)', id: 'LAST_NAME_ASCENDING' }, ], value: () => 'LAST_MODIFIED_DESCENDING', + mode: 'advanced', }, ], tools: { diff --git a/apps/sim/tools/google_contacts/create.ts b/apps/sim/tools/google_contacts/create.ts index 939fc739a0..7ff624626f 100644 --- a/apps/sim/tools/google_contacts/create.ts +++ b/apps/sim/tools/google_contacts/create.ts @@ -1,3 +1,4 @@ +import { createLogger } from '@sim/logger' import { DEFAULT_PERSON_FIELDS, type GoogleContactsCreateParams, @@ -7,6 +8,8 @@ import { } from '@/tools/google_contacts/types' import type { ToolConfig } from '@/tools/types' +const logger = createLogger('GoogleContactsCreate') + export const createTool: ToolConfig = { id: 'google_contacts_create', name: 'Google Contacts Create', @@ -125,6 +128,13 @@ export const createTool: ToolConfig { const data = await response.json() + + if (!response.ok) { + const errorMessage = data.error?.message || 'Failed to create contact' + logger.error('Failed to create contact', { status: response.status, error: errorMessage }) + throw new Error(errorMessage) + } + const contact = transformPerson(data) return { diff --git a/apps/sim/tools/google_contacts/delete.ts b/apps/sim/tools/google_contacts/delete.ts index ab4bcaf935..92f02cd339 100644 --- a/apps/sim/tools/google_contacts/delete.ts +++ b/apps/sim/tools/google_contacts/delete.ts @@ -1,3 +1,4 @@ +import { createLogger } from '@sim/logger' import { type GoogleContactsDeleteParams, type GoogleContactsDeleteResponse, @@ -5,6 +6,8 @@ import { } from '@/tools/google_contacts/types' import type { ToolConfig } from '@/tools/types' +const logger = createLogger('GoogleContactsDelete') + export const deleteTool: ToolConfig = { id: 'google_contacts_delete', name: 'Google Contacts Delete', @@ -33,7 +36,7 @@ export const deleteTool: ToolConfig - `${PEOPLE_API_BASE}/${params.resourceName}:deleteContact`, + `${PEOPLE_API_BASE}/${params.resourceName.trim()}:deleteContact`, method: 'DELETE', headers: (params: GoogleContactsDeleteParams) => ({ Authorization: `Bearer ${params.accessToken}`, @@ -56,7 +59,9 @@ export const deleteTool: ToolConfig = { id: 'google_contacts_get', name: 'Google Contacts Get', @@ -35,7 +38,7 @@ export const getTool: ToolConfig - `${PEOPLE_API_BASE}/${params.resourceName}?personFields=${DEFAULT_PERSON_FIELDS}`, + `${PEOPLE_API_BASE}/${params.resourceName.trim()}?personFields=${DEFAULT_PERSON_FIELDS}`, method: 'GET', headers: (params: GoogleContactsGetParams) => ({ Authorization: `Bearer ${params.accessToken}`, @@ -45,6 +48,13 @@ export const getTool: ToolConfig { const data = await response.json() + + if (!response.ok) { + const errorMessage = data.error?.message || 'Failed to get contact' + logger.error('Failed to get contact', { status: response.status, error: errorMessage }) + throw new Error(errorMessage) + } + const contact = transformPerson(data) return { diff --git a/apps/sim/tools/google_contacts/list.ts b/apps/sim/tools/google_contacts/list.ts index 05b32e80e0..0509b74599 100644 --- a/apps/sim/tools/google_contacts/list.ts +++ b/apps/sim/tools/google_contacts/list.ts @@ -1,3 +1,4 @@ +import { createLogger } from '@sim/logger' import { DEFAULT_PERSON_FIELDS, type GoogleContactsListParams, @@ -7,6 +8,8 @@ import { } from '@/tools/google_contacts/types' import type { ToolConfig } from '@/tools/types' +const logger = createLogger('GoogleContactsList') + export const listTool: ToolConfig = { id: 'google_contacts_list', name: 'Google Contacts List', @@ -65,6 +68,13 @@ export const listTool: ToolConfig { const data = await response.json() + + if (!response.ok) { + const errorMessage = data.error?.message || 'Failed to list contacts' + logger.error('Failed to list contacts', { status: response.status, error: errorMessage }) + throw new Error(errorMessage) + } + const connections = data.connections || [] const contacts = connections.map((person: Record) => transformPerson(person)) diff --git a/apps/sim/tools/google_contacts/search.ts b/apps/sim/tools/google_contacts/search.ts index 226426c3e2..79a718f641 100644 --- a/apps/sim/tools/google_contacts/search.ts +++ b/apps/sim/tools/google_contacts/search.ts @@ -1,3 +1,4 @@ +import { createLogger } from '@sim/logger' import { DEFAULT_PERSON_FIELDS, type GoogleContactsSearchParams, @@ -7,6 +8,8 @@ import { } from '@/tools/google_contacts/types' import type { ToolConfig } from '@/tools/types' +const logger = createLogger('GoogleContactsSearch') + export const searchTool: ToolConfig = { id: 'google_contacts_search', name: 'Google Contacts Search', @@ -58,6 +61,13 @@ export const searchTool: ToolConfig { const data = await response.json() + + if (!response.ok) { + const errorMessage = data.error?.message || 'Failed to search contacts' + logger.error('Failed to search contacts', { status: response.status, error: errorMessage }) + throw new Error(errorMessage) + } + const results = data.results || [] const contacts = results.map((result: Record) => transformPerson(result.person || result) diff --git a/apps/sim/tools/google_contacts/update.ts b/apps/sim/tools/google_contacts/update.ts index 33fed59f50..51d50448d7 100644 --- a/apps/sim/tools/google_contacts/update.ts +++ b/apps/sim/tools/google_contacts/update.ts @@ -1,3 +1,4 @@ +import { createLogger } from '@sim/logger' import { DEFAULT_PERSON_FIELDS, type GoogleContactsUpdateParams, @@ -7,6 +8,8 @@ import { } from '@/tools/google_contacts/types' import type { ToolConfig } from '@/tools/types' +const logger = createLogger('GoogleContactsUpdate') + export const updateTool: ToolConfig = { id: 'google_contacts_update', name: 'Google Contacts Update', @@ -108,7 +111,7 @@ export const updateTool: ToolConfig ({ @@ -156,6 +159,13 @@ export const updateTool: ToolConfig { const data = await response.json() + + if (!response.ok) { + const errorMessage = data.error?.message || 'Failed to update contact' + logger.error('Failed to update contact', { status: response.status, error: errorMessage }) + throw new Error(errorMessage) + } + const contact = transformPerson(data) return {