Agent Skills: HubSpot Reliability Patterns

|

UncategorizedID: jeremylongshore/claude-code-plugins-plus-skills/hubspot-reliability-patterns

Install this agent skill to your local

pnpm dlx add-skill https://github.com/jeremylongshore/claude-code-plugins-plus-skills/tree/HEAD/plugins/saas-packs/hubspot-pack/skills/hubspot-reliability-patterns

Skill Files

Browse the full folder contents for hubspot-reliability-patterns.

Download Skill

Loading file tree…

plugins/saas-packs/hubspot-pack/skills/hubspot-reliability-patterns/SKILL.md

Skill Metadata

Name
hubspot-reliability-patterns
Description
|

HubSpot Reliability Patterns

Overview

Production-grade reliability patterns for HubSpot CRM integrations: circuit breaker, retry with Retry-After, graceful degradation, and dead letter queues.

Prerequisites

  • @hubspot/api-client installed (has built-in retry)
  • Optional: opossum for circuit breaker
  • Optional: Redis or database for dead letter queue

Instructions

Step 1: SDK Built-in Retry (First Line of Defense)

import * as hubspot from '@hubspot/api-client';

// The SDK automatically retries 429 and 5xx errors
const client = new hubspot.Client({
  accessToken: process.env.HUBSPOT_ACCESS_TOKEN!,
  numberOfApiCallRetries: 3,  // retries with exponential backoff
});
// This handles most transient failures automatically

Step 2: Circuit Breaker for HubSpot API

import CircuitBreaker from 'opossum';

// Circuit breaker wrapping all HubSpot API calls
const hubspotBreaker = new CircuitBreaker(
  async <T>(operation: () => Promise<T>): Promise<T> => operation(),
  {
    timeout: 15000,                // 15s timeout per call
    errorThresholdPercentage: 50,  // open after 50% failure rate
    resetTimeout: 30000,           // try again after 30s
    volumeThreshold: 5,            // need 5+ calls before evaluating
    rollingCountTimeout: 60000,    // 60s rolling window
  }
);

// Monitor circuit state
hubspotBreaker.on('open', () => {
  console.warn('[HubSpot] Circuit OPEN -- requests failing fast');
  // Alert team: HubSpot integration degraded
});

hubspotBreaker.on('halfOpen', () => {
  console.info('[HubSpot] Circuit HALF-OPEN -- testing recovery');
});

hubspotBreaker.on('close', () => {
  console.info('[HubSpot] Circuit CLOSED -- normal operation');
});

// Usage
async function resilientHubSpotCall<T>(operation: () => Promise<T>): Promise<T> {
  return hubspotBreaker.fire(operation) as Promise<T>;
}

// Example
const contacts = await resilientHubSpotCall(() =>
  client.crm.contacts.basicApi.getPage(10, undefined, ['email'])
);

Step 3: Graceful Degradation

// Serve cached/fallback data when HubSpot is unavailable
import { LRUCache } from 'lru-cache';

const fallbackCache = new LRUCache<string, any>({
  max: 10000,
  ttl: 30 * 60 * 1000, // 30 minutes
});

async function withFallback<T>(
  cacheKey: string,
  operation: () => Promise<T>,
  fallback?: T
): Promise<{ data: T; source: 'live' | 'cache' | 'fallback' }> {
  try {
    const data = await resilientHubSpotCall(operation);
    fallbackCache.set(cacheKey, data);
    return { data, source: 'live' };
  } catch (error) {
    // Try cache first
    const cached = fallbackCache.get(cacheKey);
    if (cached) {
      console.warn(`[HubSpot] Serving cached data for ${cacheKey}`);
      return { data: cached as T, source: 'cache' };
    }

    // Use static fallback if provided
    if (fallback !== undefined) {
      console.warn(`[HubSpot] Serving fallback for ${cacheKey}`);
      return { data: fallback, source: 'fallback' };
    }

    throw error;
  }
}

// Usage
const { data: contacts, source } = await withFallback(
  'recent-contacts',
  () => client.crm.contacts.basicApi.getPage(10, undefined, ['email', 'firstname']),
  { results: [], paging: undefined } // empty fallback
);

if (source !== 'live') {
  console.warn(`Serving ${source} data -- HubSpot may be degraded`);
}

Step 4: Dead Letter Queue

interface FailedOperation {
  id: string;
  operation: string;
  payload: any;
  error: string;
  correlationId?: string;
  attempts: number;
  firstAttempt: string;
  lastAttempt: string;
}

class HubSpotDeadLetterQueue {
  private queue: FailedOperation[] = [];

  add(operation: string, payload: any, error: any): void {
    const entry: FailedOperation = {
      id: `dlq-${Date.now()}-${Math.random().toString(36).slice(2)}`,
      operation,
      payload,
      error: error?.body?.message || error.message,
      correlationId: error?.body?.correlationId,
      attempts: 1,
      firstAttempt: new Date().toISOString(),
      lastAttempt: new Date().toISOString(),
    };
    this.queue.push(entry);
    console.warn(`[DLQ] Enqueued ${operation}: ${entry.error}`);
  }

  async retryAll(): Promise<{ succeeded: number; failed: number }> {
    let succeeded = 0, failed = 0;

    for (const entry of [...this.queue]) {
      try {
        // Retry the operation
        await this.executeOperation(entry);
        this.queue = this.queue.filter(e => e.id !== entry.id);
        succeeded++;
      } catch (error) {
        entry.attempts++;
        entry.lastAttempt = new Date().toISOString();
        failed++;

        if (entry.attempts > 5) {
          console.error(`[DLQ] Giving up on ${entry.id} after ${entry.attempts} attempts`);
          // Move to permanent failure storage
        }
      }
    }

    return { succeeded, failed };
  }

  private async executeOperation(entry: FailedOperation): Promise<void> {
    switch (entry.operation) {
      case 'createContact':
        await client.crm.contacts.basicApi.create({
          properties: entry.payload,
          associations: [],
        });
        break;
      case 'updateContact':
        await client.crm.contacts.basicApi.update(
          entry.payload.id, { properties: entry.payload.properties }
        );
        break;
      // Add more operation types as needed
    }
  }

  getStats(): { pending: number; oldestAge: string } {
    return {
      pending: this.queue.length,
      oldestAge: this.queue.length > 0
        ? this.queue[0].firstAttempt
        : 'none',
    };
  }
}

const dlq = new HubSpotDeadLetterQueue();

// Usage
async function createContactWithDLQ(properties: Record<string, string>) {
  try {
    return await resilientHubSpotCall(() =>
      client.crm.contacts.basicApi.create({ properties, associations: [] })
    );
  } catch (error) {
    dlq.add('createContact', properties, error);
    throw error;
  }
}

// Retry DLQ periodically
setInterval(async () => {
  const stats = dlq.getStats();
  if (stats.pending > 0) {
    console.log(`[DLQ] Retrying ${stats.pending} failed operations...`);
    const result = await dlq.retryAll();
    console.log(`[DLQ] Results: ${result.succeeded} succeeded, ${result.failed} failed`);
  }
}, 5 * 60 * 1000); // every 5 minutes

Step 5: Health Check with Degraded State

type HealthStatus = 'healthy' | 'degraded' | 'unhealthy';

async function hubspotHealthCheck(): Promise<{
  status: HealthStatus;
  circuitState: string;
  dlqPending: number;
  apiLatencyMs: number;
}> {
  const dlqStats = dlq.getStats();
  const start = Date.now();

  try {
    await client.crm.contacts.basicApi.getPage(1);
    const latency = Date.now() - start;

    const status: HealthStatus =
      hubspotBreaker.stats().state === 'open' ? 'degraded' :
      dlqStats.pending > 50 ? 'degraded' :
      latency > 5000 ? 'degraded' :
      'healthy';

    return {
      status,
      circuitState: hubspotBreaker.stats().state,
      dlqPending: dlqStats.pending,
      apiLatencyMs: latency,
    };
  } catch {
    return {
      status: 'unhealthy',
      circuitState: hubspotBreaker.stats().state,
      dlqPending: dlqStats.pending,
      apiLatencyMs: Date.now() - start,
    };
  }
}

Output

  • SDK built-in retry handling transient 429/5xx errors
  • Circuit breaker preventing cascade failures
  • Graceful degradation with cached/fallback data
  • Dead letter queue for failed operations with automatic retry
  • Health check reporting degraded state

Error Handling

| Issue | Cause | Solution | |-------|-------|----------| | Circuit stays open | Threshold too low | Increase errorThresholdPercentage | | Stale cache served | HubSpot down for long time | Alert when cache age > TTL | | DLQ growing | Persistent failures | Investigate root cause, not symptoms | | Fallback data confusing users | No degradation indicator | Show "data may be stale" in UI |

Resources

Next Steps

For policy enforcement, see hubspot-policy-guardrails.