We use cookies to enhance your experience and measure how the site performs. Choose "Essential Only" to disable analytics. Read our Privacy Policy.

    Odeus Docs

    Creating Custom Integrations

    Custom integrations let you connect any API-enabled tool to an agent. This opens up endless possibilities. Below you find a comprehensive guide on how to build integrations, actions, and triggers.

    Creating Custom Integrations

    Custom integrations let you connect any API-enabled tool to an agent. This opens up endless possibilities. Below you find a comprehensive guide on how to build integrations, actions, and triggers.

    Integrations vs. Actions vs. Triggers

    Integrations are standardized connections between Odeus and third-party tools that handle authentication and API communication. Within each integration, you can build:

    • Actions: Functions that agents and workflows can call to interact with APIs (e.g., "create ticket", "send email", "get data")
    • Triggers: Event monitors that start workflows when specific events occur (e.g., "new email received", "file uploaded")

    Setting up an Integration

    In the integrations menu, click Add integration to get started.

    Next, specify an integration name and upload an icon (shown in chat when using actions and in the integrations overview). Add a description to help agents know when to use this integration. Hit Save to create it.

    Authentication

    Start with authentication in the Build tab. Select your authentication type and configure it following the steps below:

    API Key

    After selecting API Key authentication, add custom input fields in step 2 (like API key or client ID). These inputs are collected when users set up connections and can be marked as "required."

    Step 3 lets you set up a test API endpoint to validate authentication. Replace the URL parameter and add references to your input fields using data.auth.fieldId.

    Use the built-in ld.request and ld.log functions for requests and logging.

    Test your action and create your first connection.

    OAuth 2.0

    Custom integrations support OAuth 2.0 authentication.

    Step 2 allows custom input fields (collected during connection setup). Client ID and Client Secret are entered in step 4, so this covers additional parameters only.

    Create an OAuth client

    Set up an OAuth client/App/Project in your target application and enable the required APIs. This is application-specific, which is why our interface supports custom code in step 5.

    For Google Calendar, create a Google Service Account, generate a new key to get the client ID and secret, add them to Odeus in step 4, save the OAuth Redirect URL, and enable the Google Calendar API.

    Change Authorization URL

    Check the OAuth documentation for your service and extract the Authorization URL. Usually, changing the BASE_URL in our template is sufficient.

    For Google Calendar:

    return `https://accounts.google.com/o/oauth2/v2/auth?client_id=${env.CLIENT_ID}&response_type=code&scope=${data.input.scope}&access_type=offline&redirect_uri=${encodeURIComponent(data.input.redirectUrl)}&state=${data.input.state}&prompt=consent`;
    

    Define Scopes

    Define OAuth scopes required by your actions. List them comma or space-separated according to your API documentation.

    For Google Calendar (space-separated):

    https://www.googleapis.com/auth/calendar.events https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile
    

    Provide Access Token & Refresh Token URL

    Check your API's OAuth docs for the Access Token URL and Refresh Token URL. Usually, updating the tokenUrl in our template works.

    For Google Calendar:

    const tokenUrl ='https://oauth2.googleapis.com/token';

    Test Authentication Setup

    Provide a test API endpoint (like /me) to verify authentication. The return value of that test request can be used inside the OAuth Client Label to influence the naming of the established connections. You can access the return value via: {{data.input}}

    For Google Calendar: Google Sheets - {{data.input.useremail.value}}

    Test by adding a connection and verifying the authorization flow works.

    For Google Calendar, we test with:

    url: 'https://people.googleapis.com/v1/people/me?personFields=names,emailAddresses'
    

    Public APIs

    Choose None for publicly available APIs without authentication.

    Building Actions

    Actions allow agents to interact with your API endpoints. There are two types of actions:

    • Regular Actions: Standard API interactions (create, read, update, delete operations)
    • Native Actions: Special file search and download actions that integrate with Odeus's file system

    Regular Actions

    Regular actions are the most common type and handle standard API operations.

    When to Build Regular Actions

    • CRUD operations: Create, read, update, or delete data via API calls
    • Data processing: Send data to APIs for analysis, transformation, or validation
    • File operations: Upload files to services, process documents, send attachments
    • Notifications: Send emails, messages, or create tickets
    • Integrations: Connect multiple services or sync data between platforms

    Setting Up Regular Actions

    1. Add Action: In your integration, click "Add Action"
    2. Configure Basic Info: Set name, description, and slug
    3. Add Input Fields: Define what data the action needs from users
    4. Write Action Code: Implement the API interaction logic
    5. Test: Validate your action works correctly

    Input Field Types

    TypePurposeNotes
    TEXTShort text inputSingle line text
    MULTI_LINE_TEXTLong text inputMultiple lines, good for descriptions
    NUMBERNumeric inputIntegers or decimals
    BOOLEANTrue/false toggleCheckbox input
    SELECTDropdown optionsPre-defined choices
    FILEFile uploadSingle or multiple files (see file support guide)
    OBJECTComplex dataJSON objects with custom schema
    PASSWORDSensitive textHidden input for secrets

    Example: Create Ticket Action

    // Validate required inputs
    if (!data.input.title) {
      return { error: "Title is required" };
    }
    
    // Build request
    const options = {
      method: "POST",
      url: "https://api.ticketing-service.com/tickets",
      headers: {
        Authorization: `Bearer ${data.auth.api_key}`,
        "Content-Type": "application/json",
      },
      body: {
        title: data.input.title,
        description: data.input.description || "",
        priority: data.input.priority || "medium",
        assignee: data.input.assignee,
      },
    };
    
    try {
      const response = await ld.request(options);
    
      if (response.status === 201) {
        return {
          success: true,
          ticketId: response.json.id,
          url: response.json.url,
          message: `Created ticket #${response.json.id}: ${data.input.title}`,
        };
      } else {
        throw new Error(`API returned status ${response.status}`);
      }
    } catch (error) {
      ld.log("Error creating ticket:", error.message);
      return {
        success: false,
        error: `Failed to create ticket: ${error.message}`,
      };
    }
    

    File Upload Example

    // Handle file uploads (requires FILE input field)
    const document = data.input.document; // FileData object
    
    if (!document) {
      return { error: "Please attach a document" };
    }
    
    // Validate file type
    const allowedTypes = ["application/pdf", "image/jpeg", "image/png"];
    if (!allowedTypes.includes(document.mimeType)) {
      return {
        error: `Unsupported file type: ${
          document.mimeType
        }. Allowed: ${allowedTypes.join(", ")}`,
      };
    }
    
    const options = {
      method: "POST",
      url: "https://api.example.com/documents",
      headers: {
        Authorization: `Bearer ${data.auth.api_key}`,
        "Content-Type": "application/json",
      },
      body: {
        filename: document.fileName,
        content: document.base64,
        mimeType: document.mimeType,
      },
    };
    
    const response = await ld.request(options);
    return {
      success: true,
      documentId: response.json.id,
      message: `Uploaded ${document.fileName} successfully`,
    };
    

    Returning Files from Actions

    Actions can also generate and return files:

    // Generate CSV export
    const data = await fetchCustomerData();
    
    const csvHeader = "Name,Email,Created";
    const csvRows = data.map(
      (customer) => `"${customer.name}","${customer.email}","${customer.created}"`
    );
    const csvContent = [csvHeader, ...csvRows].join("\n");
    
    return {
      files: {
        fileName: `customers-${new Date().toISOString().slice(0, 10)}.csv`,
        mimeType: "text/csv",
        text: csvContent, // Use 'text' for UTF-8 content, 'base64' for binary
      },
      success: true,
      exported: data.length,
    };
    

    Building Triggers

    Triggers monitor external systems for events and can start workflows automatically.

    When to Build Triggers

    • Event monitoring: Detect new emails, files, records, or changes
    • Workflow automation: Start processes when specific events occur
    • Data synchronization: Keep systems in sync by detecting changes
    • Notifications: React to external events and notify users

    Trigger Types

    • Polling Triggers: Periodically check APIs for new events
    • Webhook Triggers: Receive real-time notifications from external systems

    Setting Up Polling Triggers

    1. Add Trigger: In your integration, click "Add Trigger"
    2. Configure Settings: Set name, description, and polling interval
    3. Add Input Fields: Define configuration parameters (optional)
    4. Write Trigger Code: Implement the polling logic
    5. Test: Validate your trigger detects events correctly

    Required Return Format

    Triggers must return an array of events with this structure:

    return [
      {
        id: "unique_event_id", // Required: Unique identifier
        timestamp: "2024-01-15T...", // Required: Event timestamp (ISO string)
        data: {
          // Your event data here
          eventType: "new_email",
          subject: "Important message",
          from: "[email protected]",
          // ... other event properties
        },
      },
    ];
    

    Example: New Email Trigger

    // Fetch recent emails
    const options = {
      method: "GET",
      url: "https://api.email-service.com/messages",
      headers: {
        Authorization: `Bearer ${data.auth.access_token}`,
      },
      params: {
        since: new Date(Date.now() - 60 * 60 * 1000).toISOString(), // Last hour
        limit: 10,
      },
    };
    
    try {
      const response = await ld.request(options);
      const emails = response.json.messages || [];
    
      // Transform to required format
      const results = emails.map((email) => ({
        id: email.id,
        timestamp: email.receivedAt,
        data: {
          messageId: email.id,
          subject: email.subject,
          from: email.from,
          to: email.to,
          body: email.body,
          isRead: email.isRead,
        },
      }));
    
      return results;
    } catch (error) {
      ld.log("Error fetching emails:", error.message);
      throw error;
    }
    

    Triggers with File Attachments

    When triggers detect events with files, include them in the data object. Use ld.request() to download the file content:

    const results = [];
    
    for (const email of emails) {
      const attachments = [];
    
      if (email.attachments && email.attachments.length > 0) {
        for (const attachment of email.attachments) {
          const fileResponse = await ld.request({
            method: "GET",
            url: `https://api.email-service.com/attachments/${attachment.id}`,
            headers: {
              Authorization: `Bearer ${data.auth.access_token}`,
            },
            responseType: "stream",
          });
    
          attachments.push({
            fileName: attachment.filename,
            mimeType: attachment.mimeType,
            base64: Buffer.from(fileResponse.buffer).toString("base64"),
          });
        }
      }
    
      results.push({
        id: email.id,
        timestamp: email.receivedAt,
        data: {
          subject: email.subject,
          from: email.from,
          body: email.body,
          files: attachments,
        },
      });
    }
    
    return results;
    

    Webhook Triggers

    The trigger builder currently supports polling triggers only. Webhook (REST_HOOK) triggers are available via the API but not yet exposed in the trigger builder UI.

    Native Actions

    Native actions allow you to natively search and download files that aren't stored locally on a user's device. We've already built native actions for SharePoint, OneDrive, Google Drive, and Confluence. You can access these via the Select files button to search and attach files directly to Chat or Agent Knowledge.

    <img src="https://mintcdn.com/odeus-34/yFkcGlsUCYIpoHgV/images/native_actions_chat.png?fit=max&auto=format&n=yFkcGlsUCYIpoHgV&q=85&s=f02b718caba22428b304e4f5c696394e" alt="Native Actions Chat " title="Native Actions Chat " style={{ width: "94%" }} width="1604" height="632" data-path="images/native_actions_chat.png" />

    Attach files to the chat using native integrations.

    <img src="https://mintcdn.com/odeus-34/yFkcGlsUCYIpoHgV/images/native_actions_knowledge.png?fit=max&auto=format&n=yFkcGlsUCYIpoHgV&q=85&s=e4904ddb39b2181436a9b8277404f172" alt="Native Actions Knowledge " title="Native Actions Knowledge " style={{ width: "94%" }} width="1412" height="604" data-path="images/native_actions_knowledge.png" />

    Attach files to the Agent knowledge by using a native integration.

    Building native actions for other tools enables you to search and download files from those platforms in the same way.

    Setting up a Native Action

    To set up a native action, begin building your integration as usual. Add another action, and in Step 1 under Advanced, select either "Search files" or "Download file" as the action type.

    Afterwards, you build the action as any other action, but your function needs to return a specific object structure. This ensures compatibility and enables agents to handle files and search results correctly.

    Required Output Format

    Depending on the action you select, your function must return a specific object structure. This ensures compatibility and enables agents to handle files and search results correctly.

    Search files: When building a native search integration, your function must return an array of objects matching the following schema:

    {
      url: string,
      documentId: string,
      title: string,
      author?: {
        id: string,
        name: string,
        imgUrl?: string,
      },
      mimeType: string,
      lastSeenByUser: Date,
      createdDate: Date,
      lastModifiedByAnyone: Date,
      lastModifiedByUserId?: {
        id?: string,
        name?: string,
        lastModifiedByUserIdDate: Date,
      },
      parent?: {
        id: string,
        title?: string,
        url?: string,
        type?: string,
        driveId?: string,
        siteId?: string,
        listId?: string,
        listItemId?: string,
      }
    }
    

    The title and mimeType will be displayed in the UI for all search results.

    Please also check out a detailed description for each parameter:

    Required Fields

    FieldTypeDescription
    urlstringThe web URL where users can view or edit the file. This should be a direct link that opens the file in the source application (e.g., Google Docs editor, SharePoint viewer). Must be a valid HTTPS URL that the user can access with their credentials.
    documentIdstringThe unique identifier of the file in the source system. This ID is used internally to reference the file and should remain stable across searches. Can be any string format (UUID, numeric ID, etc.) as long as it uniquely identifies the file.
    titlestringThe display name of the file. This is what users will see in search results. Should include the file extension if relevant (e.g., "Report.pdf", "Budget.xlsx").
    mimeTypestringThe MIME type of the file. Used to determine the file type icon and category (e.g. "application/pdf", "text/plain", "application/vnd.google-apps.document").

    Optional Fields

    FieldTypeDescription
    authorobjectInformation about who created the file. Helps users identify file ownership and origin.
    author.idstringUnique identifier of the author. Typically an email address or user ID in the source system.
    author.namestringDisplay name of the author. The human-readable name shown to users (e.g., "John Doe").
    lastSeenByUserstringWhen the current user last viewed this file. ISO 8601 date string (e.g., "2024-01-15T10:30:00Z"). Used for "Recently viewed" sorting. Return null or omit if the user has never viewed the file.
    createdDatestringWhen the file was originally created. ISO 8601 date string. Helps users understand file age and sort by creation date.
    lastModifiedByAnyonestringWhen the file was last modified by any user. ISO 8601 date string. Critical for identifying recently updated content and collaborative work.
    lastModifiedByUserIdobjectInformation about who last modified the file. Helps track recent changes in collaborative environments. Entire object should be omitted if any required sub-field is missing.
    lastModifiedByUserId.idstringUnique identifier of the last editor. Typically an email or user ID.
    lastModifiedByUserId.namestringDisplay name of the last editor. Human-readable name of who made the last changes.
    lastModifiedByUserId.lastModifiedByUserIdDatestringTimestamp of the last modification. ISO 8601 date string. Usually matches lastModifiedByAnyone.
    parentobjectInformation about the file's location/container. Helps users understand file organization and navigate to parent folders.
    parent.idstringUnique identifier of the parent folder/container. Used for folder-based operations and navigation.
    parent.titlestringDisplay name of the parent folder. Shown to help users understand file location (e.g., "Marketing Materials", "Q1 Reports").
    parent.urlstringWeb URL to view the parent folder. Direct link to open the folder in the source application.
    parent.typestringType of parent container. Optional classifier (e.g., "folder", "workspace", "site").
    parent.driveIdstringIdentifier of the drive/library containing the file. For services with multiple storage locations (e.g., SharePoint sites, Google Shared Drives).
    parent.siteIdstringIdentifier of the site containing the file. Specific to SharePoint and similar platforms with site-based organization.
    parent.listIdstringIdentifier of the list containing the file. For list-based storage systems.
    parent.listItemIdstringIdentifier of the list item associated with the file. For files attached to list items.
    contentPreviewstringA text snippet from the file's content. Provides context about file contents in search results. Should be plain text, typically 100-200 characters. Useful for showing relevant excerpts that match search queries. Set to null if content preview is not available.

    Usage Guidelines

    Dates:
    All date fields must be valid ISO 8601 strings or omitted entirely. Invalid dates will cause parsing errors.

    Null vs Omitted:

    • Use null for fields that are explicitly empty (e.g., no content preview available)
    • Omit fields entirely if the data is not applicable or unavailable

    Parent Information: Include as much parent information as available to help users navigate file hierarchies

    • Author Information: Always include both id and name in author objects, or omit the entire object
    • Search Relevance: Fields like contentPreview can significantly improve search UX by showing why a file matched the query

    Below you can find an example implementation for the native SharePoint Search files action.

    const entityTypes = ["driveItem"];
    const queryString = data.input.query;
    
    try {
      // Perform search if query exists
      const searchRequest = {
        requests: [
          {
            entityTypes,
            query: { queryString },
            trimDuplicates: true,
            queryAlterationOptions: {
              enableModification: true,
              enableSuggestions: true,
            },
          },
        ],
      };
    
      const searchResult = await ld.request({
        method: 'POST',
        url: 'https://graph.microsoft.com/v1.0/search/query',
        body: searchRequest,
        headers: {
          'Authorization': `Bearer ${data.auth.access_token}`,
          'Content-Type': 'application/json',
        },
      });
    
      const hits = searchResult.json?.value?.[0]?.hitsContainers?.[0]?.hits;
      if (hits && hits.length > 0) {
        const results = hits.filter((hit) => hit.resource.name).map((hit) => {
          const { resource } = hit;
          return {
            url: encodeURI(`${resource.webUrl}?web=1`),
            documentId: resource.id,
            title: resource.name,
            mimeType: getMimeTypeFromFileName(resource.name),
            author: resource.createdBy?.user ? {
              id: resource.createdBy.user.email || '',
              name: resource.createdBy.user.displayName || '',
            } : undefined,
            createdDate: resource.createdDateTime,
            lastModifiedByAnyone: resource.lastModifiedDateTime,
            lastModifiedByUserId: resource.lastModifiedBy?.user ? {
              id: resource.lastModifiedBy.user.email || '',
              name: resource.lastModifiedBy.user.displayName || '',
              lastModifiedByUserIdDate: resource.lastModifiedDateTime,
            } : undefined,
            parent: resource.parentReference ? {
              id: resource.parentReference.id || '',
              title: resource.parentReference.path ? resource.parentReference.path.split('/').pop() : undefined,
              driveId: resource.parentReference.driveId || '',
            } : undefined,
          };
        });
        return results;
      }
      return [];
    } catch (error) {
      ld.log(`Error: ${error.message}, Stack: ${error.stack}`);
      return [];
    }
    
    function getMimeTypeFromFileName(fileName) {
      const extension = fileName.split('.').pop().toLowerCase();
      const mimeTypes = {
        'txt': 'text/plain',
        'doc': 'application/msword',
        'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
        'pdf': 'application/pdf',
      };
      return mimeTypes[extension] || 'application/octet-stream';
    }
    

    Download file
    For native download actions, return an object in the following format:

    {
      fileName: string,
      mimeType: string,
      buffer: response.buffer, // Conditional: for binary files
      url: string,
      lastModified: Date,
      text: string // Conditional: for text files
    }
    

    The fileName and mimeType will be displayed in the UI for all search results.

    Please also check out a detailed description for each parameter:

    Overview

    FieldTypeRequiredDescription
    fileNamestringYesThe complete filename including extension. This is the name that will be used when saving the file. Should match the original filename from the source system (e.g., "Budget_2024.xlsx", "Design_Final.pdf"). If the file doesn't have an extension in the source, add the appropriate one based on mimeType.
    mimeTypestringYesThe MIME type identifying the file format. Determines how the file will be processed and what icon to display. Must be a valid MIME type (e.g., "application/pdf", "image/png", "text/plain"). For Google Workspace files, use the original MIME type, not the exported format's type.
    bufferBufferConditionalThe binary content of the file as a Buffer. Required for binary files (PDFs, images, Office docs). The actual file data that will be saved. Can be provided as: native Buffer object, base64 encoded string, or object with format type:"Buffer", data: [byte array].
    urlstringYesThe web URL to view/edit the file in its source application. Should be a direct link that opens the file when clicked (e.g., Google Drive viewer URL, SharePoint document URL). Used for users to access the original file and for reference tracking.
    lastModifiedstring/DateYesISO 8601 timestamp of the file's last modification. Indicates when the file content was last changed. Can be a Date object or ISO string like "2024-01-15T10:30:00Z". Used for version tracking and determining file freshness.
    textstringConditionalThe text content of the file as a UTF-8 string. Required for text-based files instead of buffer. Use for plain text, HTML, JSON, CSV, or any human-readable format. Should contain the complete file content. Cannot be used together with buffer.

    Important Notes

    Content Fields: You must provide either buffer OR text, never both:
    Use buffer for: Images, PDFs, Office documents, videos, any binary format
    Use text for: Plain text, HTML, source code, JSON, XML, any text format

    MIME Type Accuracy: The mimeType must accurately reflect the content being returned:

    • For native Google Docs exported as HTML, still use "application/vnd.google-apps.document"
    • For converted files, use the original source MIME type, not the export format

    File Naming: The fileName should:

    • Include the correct file extension
    • Match what users expect from the source system
    • Be sanitized to remove invalid filesystem characters

    URL Requirements: The url must:

    • Be accessible with the user's authentication
    • Open the file in the source application (not a download link)
    • Be a stable link that won't expire quickly

    Below you can find an example implementation for the native SharePoint Download file action.

    async function downloadOneDriveFile() {
      try {
        // Construct the API path based on the input configuration
        const config = JSON.parse(data.input.parent);
        let apiPath = '';
    
        if (config.listId && config.listItemId) {
          apiPath = `/sites/${config.siteId}/lists/${config.listId}/items/${data.input.itemId}/driveItem`;
        } else if (config.driveId) {
          apiPath = `/drives/${config.driveId}/items/${data.input.itemId}`;
        } else if (config.groupId) {
          apiPath = `/groups/${config.groupId}/drive/items/${data.input.itemId}`;
        } else if (config.userId) {
          apiPath = `/users/${config.userId}/drive/items/${data.input.itemId}`;
        } else if (config.siteId) {
          apiPath = `/sites/${config.siteId}/drive/items/${data.input.itemId}`;
        } else {
          throw new Error('Insufficient information to construct API path');
        }
    
        // Make the request to get the file metadata including download URL
        const options = {
          method: 'GET',
          url: `https://graph.microsoft.com/v1.0${apiPath}`,
          headers: {
            'Authorization': 'Bearer ' + data.auth.access_token,
            'Accept': 'application/json',
          },
        };
    
        const response = await ld.request(options);
    
        if (response.json['@microsoft.graph.downloadUrl']) {
          const downloadUrl = response.json['@microsoft.graph.downloadUrl'];
    
          // Request to download the file content
          const contentOptions = {
            method: 'GET',
            url: downloadUrl,
            responseType: 'stream'
          };
    
          const contentResponse = await ld.request(contentOptions);
    
          if (contentResponse.status !== 200) {
            throw new Error(
              `Error fetching file content: ${JSON.stringify(contentResponse)}`
            );
          }
    
          return {
            fileName: response.json.name,
            mimeType: response.json.file.mimeType,
            buffer: contentResponse.buffer,
            url: response.json.webUrl,
            lastModified: response.json.lastModifiedDateTime,
          };
        } else {
          throw new Error('Could not download file!');
        }
      } catch (error) {
        ld.log('Error downloading item from OneDrive: ' + error.message);
        throw error;
      }
    }
    
    return downloadOneDriveFile();
    

    Accessing Input Fields

    Use data.input.{fieldSlug} for input field values and data.auth.{fieldSlug} for authentication field values from the user's current connection. The slug is the identifier you set when creating each field in the integration builder.

    Built-in Functions for Custom Code Sections

    Use our Integration Agent to help set up your integration functions.

    Custom code sections have access to a set of built-in utility functions for common operations. Here are the most commonly used:

    Essential Functions

    • ld.request() - Make HTTP requests to external APIs
    • ld.log() - Output debugging information
    • atob() / btoa() - Base64 encoding/decoding
    • JSON.stringify() / JSON.parse() - JSON manipulation
    • Complete Sandbox Utilities Reference — View all available sandbox utilities including data conversions (CSV, Parquet, Arrow), SQL validation, cryptography, AWS request signing, Microsoft XMLA integration, and more.

    Quick Examples

    HTTP Request

    const options = {
      method: "GET",
      url: `https://www.googleapis.com/calendar/v3/calendars/${data.input.calendarId}/events/${data.input.eventId}`,
      headers: {
        Authorization: "Bearer " + data.auth.access_token,
        Accept: "application/json",
      },
    };
    
    const response = await ld.request(options);
    return response.json;
    

    JSON Parsing

    const properties = data.input.properties
      ? JSON.parse(data.input.properties)
      : {};
    
    const options = {
      method: "PATCH",
      url: `https://api.hubapi.com/crm/v3/objects/companies/${data.input.companyId}`,
      headers: {
        Authorization: "Bearer " + data.auth.access_token,
        "Content-Type": "application/json",
      },
      body: { properties },
    };
    

    Base64 Encoding

    const auth = btoa(`${env.CLIENT_ID}:${env.CLIENT_SECRET}`);
    

    Sandbox Library Restrictions

    Custom integration code runs in a secure sandboxed environment. You cannot install or import external libraries (npm, pip, etc.) - only a limited set of built-in JavaScript/Node.js APIs are available. For advanced processing (e.g., PDF parsing, image manipulation), use external APIs or services and call them from your integration code.

    Best Practices

    Action Design

    • Single responsibility: Each action should do one thing well
    • Clear naming: Use descriptive action names that explain the purpose
    • Input validation: Always validate required inputs and provide helpful error messages
    • Error handling: Catch and handle API errors gracefully
    • Logging: Use ld.log() to help with debugging

    ID Handling

    Most API calls require specific internal IDs. The challenge is that agents can't guess these IDs, which creates a poor user experience when calling actions like "get specific contact in HubSpot" or "add event to specific calendar in Google Calendar."

    The solution: Create helper actions that retrieve and return these IDs to the agent first. For example, our Get deal context function for HubSpot uses GET endpoints to gather internal IDs for available pipelines and stages. This enables agents to use actions like Create deal or Update deal much more effectively since they now have the required context.

    Performance

    • Minimize API calls: Batch operations when possible
    • Use pagination: Handle large datasets appropriately
    • Timeout handling: Set appropriate timeouts for external API calls

    Security

    • Validate inputs: Never trust user input without validation
    • Sanitize data: Clean data before sending to external APIs
    • Handle secrets: Use authentication fields for sensitive data, never hardcode
    • Rate limiting: Respect API rate limits and implement backoff strategies