Agent skill

Hubspot Product Event Sync

by jeremylongshore in jeremylongshore/tons-of-skills-marketplace

Sync backend product events into HubSpot contact and company custom properties using idempotent batched updates — the Segment integration pattern without Segment.

MITAuto-check passedSales & Support

Install Hubspot Product Event Sync

skills CLI
$ npx skills add jeremylongshore/tons-of-skills-marketplace --skill hubspot-product-event-sync -a claude-code

Project install by default; add -g for ~/.claude/skills/.

GitHub CLI
$ gh skill install jeremylongshore/tons-of-skills-marketplace hubspot-product-event-sync --agent claude-code

Project scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).

Manual copy
$ git clone --depth 1 https://github.com/jeremylongshore/tons-of-skills-marketplace.git skills-src && mkdir -p .claude/skills && cp -r skills-src/skills/.curated/hubspot-product-event-sync .claude/skills/hubspot-product-event-sync && rm -rf skills-src

Use ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.

Claude Code skills documentation · loads skills from .claude/skills/

Facts

Skill name
hubspot-product-event-sync
GitHub stars
2.8k
Token cost
~5.6k tokens
SKILL.md length
1,487 words
Files
4 (incl. references)
Skills in repo
3,342
Repo updated
First seen
Licence
MIT

At a glance

Sync backend product events into HubSpot contact and company custom properties using idempotent batched updates — the Segment integration pattern without Segment.

  • Works in 7 steps: Decide: custom property or custom… → Verify and create property definitions → Idempotency key design → …
  • You need to push server-side behavioral signals (feature usage
  • SKILL.md covers Overview, Prerequisites, Instructions and Error Handling, plus 3 more sections
  • Calls curl, psql and python3; reaches api.hubapi.com; needs HUBSPOT_ACCESS_TOKEN

What it does

Hubspot Product Event Sync is an agent skill from jeremylongshore/tons-of-skills-marketplace. Sync backend product events into HubSpot contact and company custom properties using idempotent batched updates — the Segment integration pattern without Segment. Use when you need to push server-side behavioral signals (feature usage, session counts, last-seen timestamps, trial milestones) into HubSpot CRM properties so sales and marketing can act on product data without a CDP in the stack. Trigger with "hubspot product events", "sync events to hubspot", "hubspot custom properties", "hubspot event pipeline"…

Its SKILL.md is about 5.6k tokens, which your agent loads only when the skill is triggered. The skill folder holds 4 other files, including reference files (for example `references/API_REFERENCE.md`, `references/event-queue-implementation.md` and `references/implementation-guide.md`). Compatibility notes: Designed for Claude Code

It sits in Sales & Support, covering CRM management. It works with HubSpot. The repository describes itself as: Model-agnostic agent-skills platform with a harness-free canonical layer, verified adapters, and the ccpi package manager. Explore at tonsofskills.com. The licence is MIT.

When your agent uses it

  • You need to push server-side behavioral signals (feature usage
  • Last-seen timestamps
  • Trial milestones) into HubSpot CRM properties so sales and marketing can act on product data without a CDP in the stack
  • With hubspot product events

Example prompts

  • “hubspot product events”
  • “sync events to hubspot”
  • “hubspot custom properties”
  • “/hubspot-product-event-sync”

Requirements

  • Python 3
  • Node.js
  • Compatibility (from SKILL.md): Designed for Claude Code
  • Pre-approved tools (allowed-tools): Read, Write, Edit, Bash(curl:*), Bash(jq:*), Bash(python3:*), Grep

Workflow steps

7 steps, taken from the step headings in SKILL.md.

  1. Decide: custom property or custom behavioral event?
  2. Verify and create property definitions
  3. Idempotency key design
  4. Event queue with batching and rate limiting
  5. 207 Multi-Status handling with dead-letter queue
  6. Auto-create contact on miss
  7. Rate limit wiring

What it can do on your machine

Read from SKILL.md and the folder at commit cfae287. It shows what the files ask for, not the result of running them.

  • Tool permissions

    Pre-approves these tools, so the agent can use them without asking each time:

    • Read
    • Write
    • Edit
    • Bash(curl:*)
    • Bash(jq:*)
    • Bash(python3:*)
    • Grep

    From allowed-tools in the SKILL.md frontmatter.

  • Runs code

    Shell commands in SKILL.md call:

    • curl
    • psql
    • python3
    • jq

    From the folder's file list and the shell code blocks in SKILL.md.

  • Network

    Hosts in commands or code, which the agent is likely to contact:

    • api.hubapi.com

    Also links to:

    • developers.hubspot.com

    From URLs in SKILL.md, links to its own repository left out.

  • Credentials

    Names these keys or tokens, usually read from environment variables:

    • HUBSPOT_ACCESS_TOKEN

    From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.

  • Compatibility

    Designed for Claude Code

    From compatibility in the SKILL.md frontmatter.

Context cost

Hubspot Product Event Sync loads about 5.6k tokens when it runs, and up to ~16k if it reads all its reference files. Until then it costs about 151 tokens; SKILL.md has 1,487 words of instructions outside code blocks.

Always · name and description, kept in context so the agent knows when to use it
~151
When it runs · the whole SKILL.md, loaded when a task matches
~5.6k
With references · SKILL.md plus every file in references/, read only if the agent opens them
~16k

Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.

Safety

Auto-check passed

The automated check found no risky patterns in SKILL.md.

Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.

SKILL.md

The full file from jeremylongshore/tons-of-skills-marketplace at commit cfae287, republished under its MIT licence (© jeremylongshore). 1,487 words, ~5,616 tokens.

Download SKILL.mdSave it as .claude/skills/hubspot-product-event-sync/SKILL.md (or your agent's skills folder). This skill also uses 3 other files; get the full folder from GitHub.
name
hubspot-product-event-sync
description
Sync backend product events into HubSpot contact and company custom properties using idempotent batched updates — the Segment integration pattern without Segment. Use when you need to push server-side behavioral signals (feature usage, session counts, last-seen timestamps, trial milestones) into HubSpot CRM properties so sales and marketing can act on product data without a CDP in the stack. Trigger with "hubspot product events", "sync events to hubspot", "hubspot custom properties", "hubspot event pipeline", "hubspot segment alternative", "push usage data to hubspot".
allowed-tools
Read, Write, Edit, Bash(curl:*), Bash(jq:*), Bash(python3:*), Grep
compatibility
Designed for Claude Code
version
2.9.0
license
MIT
author
Jeremy Longshore <jeremy@intentsolutions.io>
tags
hubspot, product-analytics, event-sync, integration-engineering

HubSpot Product Event Sync

Overview

Push backend product events into HubSpot custom contact and company properties — the core pattern of a Segment-style integration, built directly against the HubSpot CRM API without a CDP middleman. This is not a tutorial on HubSpot setup. It is the code your data pipeline runs at 3am when a product launch generates a 10K events/minute storm, when a network hiccup causes the same batch to be retried and your "total sessions" counter doubles, when a property type mismatch silently truncates numbers, and when your contact lookup fails because a new user signed up ten seconds ago and HubSpot doesn't have them yet.

The six production failures this skill prevents:

  1. Non-idempotent updates — the same event processed twice (retry after network failure) increments a counter twice. "Last seen" survives duplication; "total sessions" does not. Idempotency keys must be event-level, not request-level.
  2. Property type mismatch — writing a number to a HubSpot string property returns HTTP 200 with the value coerced silently. The stored data is wrong; no error surfaces. Validate property types before writing.
  3. Rate-limit burnout from event storms — a product launch generates 10K events/minute. Naive sync exhausts the 100 req/10s burst budget in under two seconds. A token-bucket queue is non-optional.
  4. Contact not found — the product event carries an email that HubSpot does not have yet. The batch update silently drops the record. Upsert-by-email or auto-create is the correct path.
  5. Batch partial failure (207 Multi-Status) — POST /crm/v3/objects/contacts/batch/update returns 207 when some records succeed and others fail. Treating 207 as success causes silent data loss at scale. Parse the per-object errors array.
  6. Custom event vs custom property confusion — HubSpot has two different systems: custom behavioral events (Marketing Hub Enterprise, timeline-visible) and custom contact/company properties (available on all tiers, stored as structured data on the record). Using the wrong mechanism for the use case leads to wrong attribution, missing data, or a surprise $3K/month plan upgrade.

Prerequisites

  • Node.js 18+ or Python 3.10+
  • HubSpot private app token with scopes: crm.objects.contacts.read, crm.objects.contacts.write, crm.objects.companies.read, crm.objects.companies.write, crm.schemas.contacts.read, crm.schemas.contacts.write
  • Custom properties already defined in HubSpot (or use the property-create flow in this skill to create them)
  • Your backend event stream: Kafka topic, SQS queue, webhook receiver, or polling loop — the sync layer is transport-agnostic
  • A dead-letter store for failed records: a Postgres table, Redis sorted set, or S3 prefix — anything you can replay from

Instructions

Build in this order. Each section neutralizes one production failure mode.

1. Decide: custom property or custom behavioral event?

Get this wrong and you build the right pipeline into the wrong HubSpot system. Custom properties and custom behavioral events are entirely separate surfaces.

DimensionCustom propertyCustom behavioral event
HubSpot tierAll tiersMarketing Hub Enterprise only
Visible inContact/company record sidebarRecord timeline + Behavioral Events report
Query in listsYes (contact.hs_last_active_date > 7d ago)Limited (via list enrollment criteria)
AttributionNo native attributionYes (can be tied to campaign attribution)
API/crm/v3/objects/contacts/batch/update/events/v3/send
Best forProduct data, counts, timestamps, tier flagsMarketing touchpoints, funnel stages, UTM-tagged actions
Wrong forTimeline visibility of behavioral sequencesQuerying by property value in segmentation lists

Rule of thumb: if your sales team needs to filter contacts by a product signal (e.g., "show me contacts where last_active_date > 14 days ago"), it goes in a custom property. If your marketing team needs to see a behavioral sequence on the contact timeline, it goes in a custom behavioral event. Most product-to-CRM pipelines use custom properties exclusively.

2. Verify and create property definitions

Before writing any data, confirm that the target property exists and has the right type. HubSpot returns 200 even when coercing an incompatible value, so the validation must happen on your side.

typescript
type HubSpotPropertyType = "string" | "number" | "date" | "datetime" | "bool" | "enumeration";

interface PropertyDefinition {
  name: string;
  type: HubSpotPropertyType;
  fieldType: "text" | "number" | "date" | "booleancheckbox" | "select" | "textarea";
  label: string;
  groupName: string;
  description?: string;
}

async function ensureProperty(
  token: string,
  objectType: "contacts" | "companies",
  prop: PropertyDefinition,
): Promise<void> {
  // Check if it exists first
  const check = await fetch(
    `https://api.hubapi.com/crm/v3/properties/${objectType}/${prop.name}`,
    { headers: { Authorization: `Bearer ${token}` } },
  );

  if (check.status === 200) {
    const existing = await check.json();
    if (existing.type !== prop.type) {
      throw new Error(
        `Property type mismatch: ${prop.name} is ${existing.type} in HubSpot, ` +
        `but your schema declares it as ${prop.type}. ` +
        `Mismatched writes return 200 with silently wrong data. ` +
        `Either rename the property or migrate the type — you cannot update a property type in place.`,
      );
    }
    return; // exists and type matches — nothing to do
  }

  if (check.status !== 404) {
    throw new Error(`Unexpected status ${check.status} checking property ${prop.name}`);
  }

  // Create it
  const create = await fetch(
    `https://api.hubapi.com/crm/v3/properties/${objectType}`,
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${token}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        name: prop.name,
        label: prop.label,
        type: prop.type,
        fieldType: prop.fieldType,
        groupName: prop.groupName,
        description: prop.description ?? "",
      }),
    },
  );

  if (!create.ok) {
    throw new Error(`Failed to create property ${prop.name}: ${await create.text()}`);
  }
}

Call ensureProperty at service startup, not per-event. Property definitions are stable; checking them on every event wastes rate-limit budget and adds latency.

3. Idempotency key design

An idempotency key is the contract that makes retry safe. The key must uniquely identify a specific value written to a specific property for a specific event occurrence. The wrong key design causes either over-deduplication (missing legitimate updates) or under-deduplication (counter inflation on retry).

typescript
import { createHash } from "crypto";

interface ProductEvent {
  eventId: string;          // UUID from your event stream — unique per occurrence
  email: string;
  properties: Record<string, string | number | boolean>;
  occurredAt: number;       // Unix ms
}

// Idempotency key = hash(eventId + propertyName)
// Scope per-property, not per-event, so you can track exactly which property write was retried
function idempotencyKey(eventId: string, propertyName: string): string {
  return createHash("sha256")
    .update(`${eventId}:${propertyName}`)
    .digest("hex")
    .slice(0, 16); // 16 hex chars = 8 bytes = 64 bits of collision resistance
}

// Store processed keys with TTL to cap memory. Redis SETEX is canonical.
// Postgres alternative: INSERT INTO idempotency_keys (key, processed_at) ON CONFLICT DO NOTHING
async function isAlreadyProcessed(key: string, redis: RedisClient): Promise<boolean> {
  return (await redis.get(`hs_sync:${key}`)) !== null;
}

async function markProcessed(key: string, redis: RedisClient): Promise<void> {
  // 48h TTL — cover the worst realistic retry window
  await redis.setex(`hs_sync:${key}`, 172_800, "1");
}

Counter properties require special handling. You cannot make "increment by 1" idempotent with a simple deduplication key on the write side, because HubSpot properties are absolute values, not deltas. The only safe pattern is:

  1. Read the current value from HubSpot before writing.
  2. Compute the new value in your code.
  3. Write the absolute new value.
  4. Use the idempotency key to skip the entire operation (read + write) if this event was already processed.
typescript
async function incrementCounter(
  token: string,
  contactId: string,
  propertyName: string,
  delta: number,
  idempotencyKeyValue: string,
  redis: RedisClient,
): Promise<void> {
  if (await isAlreadyProcessed(idempotencyKeyValue, redis)) return;

  // Read current value
  const res = await fetch(
    `https://api.hubapi.com/crm/v3/objects/contacts/${contactId}?properties=${propertyName}`,
    { headers: { Authorization: `Bearer ${token}` } },
  );
  const contact = await res.json();
  const current = parseFloat(contact.properties[propertyName] ?? "0");
  const next = current + delta;

  // Write absolute value
  await fetch(`https://api.hubapi.com/crm/v3/objects/contacts/${contactId}`, {
    method: "PATCH",
    headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
    body: JSON.stringify({ properties: { [propertyName]: String(next) } }),
  });

  await markProcessed(idempotencyKeyValue, redis);
}
4. Event queue with batching and rate limiting

A token-bucket queue that collects events for up to 5 seconds or until 100 accumulate, then flushes one batch. One batch = one API call. This converts an event storm of 10K events/minute into 100 API calls/minute — well within the 600 calls/minute budget.

The queue's contract, all load-bearing:

  • push() flushes immediately at 100 events (the HubSpot batch/update limit) or after a 5s timer, whichever comes first — never one call per event.
  • One flush = one crm/v3/objects/contacts/batch/upsert call keyed on idProperty: "email" (auto-creates on miss), wrapped in withRetry.
  • Every property value is serialized to a string — the batch API rejects non-string values.
  • The response goes to the 207 Multi-Status handler (next section); failed rows land in the dead-letter queue, never silently dropped.

Full HubSpotEventSyncQueue class: references/event-queue-implementation.md.

Show full SKILL.md (597 more words)Show less
5. 207 Multi-Status handling with dead-letter queue

This is the most commonly mishandled failure mode. HubSpot returns HTTP 207 when a batch partially succeeds. The response body contains a results array (successes) and an errors array (failures). Your code must parse both.

typescript
interface HubSpot207Response {
  status: "COMPLETE" | "PENDING" | "PROCESSING" | "CANCELED";
  results: Array<{ id: string; properties: Record<string, string> }>;
  errors: Array<{
    status: string;
    category: string;
    message: string;
    context: { id?: string[] };
  }>;
  startedAt: string;
  completedAt: string;
}

async function handle207(
  res: Response,
  events: ProductEvent[],
  dlq: DeadLetterQueue,
): Promise<void> {
  // 200 = all succeeded; 207 = partial; 400+ = all failed
  if (res.status === 200) return;

  if (res.status === 207) {
    const body: HubSpot207Response = await res.json();

    // Log successes (optional — useful for counters)
    const successIds = new Set(body.results.map((r) => r.id));

    // Route failures to DLQ with full context for replay
    for (const err of body.errors) {
      const failedId = err.context.id?.[0];
      const failedEvent = events.find((e) => e.email === failedId);
      console.error("HubSpot batch partial failure", {
        error: err.message,
        category: err.category,
        contactId: failedId,
        email: failedEvent?.email,
      });
      if (failedEvent) {
        await dlq.push({
          event: failedEvent,
          error: err,
          failedAt: new Date().toISOString(),
          retryCount: 0,
        });
      }
    }

    return;
  }

  // Non-2xx — entire batch failed
  const body = await res.text();
  console.error("HubSpot batch entirely failed", { status: res.status, body });

  // Route all events to DLQ
  for (const event of events) {
    await dlq.push({ event, error: { message: body, status: res.status }, failedAt: new Date().toISOString(), retryCount: 0 });
  }
}
6. Auto-create contact on miss

When an event arrives for an email that is not yet in HubSpot, batch/update silently drops the record. The correct path is batch/upsert (used in section 4 above) which creates the contact if the email is not found. If you are using batch/update for other reasons, implement an explicit create-on-miss fallback:

typescript
async function upsertContactByEmail(
  token: string,
  email: string,
  properties: Record<string, string>,
): Promise<string> {
  // Try to find by email first
  const search = await fetch("https://api.hubapi.com/crm/v3/objects/contacts/search", {
    method: "POST",
    headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
    body: JSON.stringify({
      filterGroups: [{
        filters: [{ propertyName: "email", operator: "EQ", value: email }],
      }],
      properties: ["email"],
      limit: 1,
    }),
  });

  const searchResult = await search.json();

  if (searchResult.total > 0) {
    // Exists — update
    const contactId = searchResult.results[0].id;
    await fetch(`https://api.hubapi.com/crm/v3/objects/contacts/${contactId}`, {
      method: "PATCH",
      headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
      body: JSON.stringify({ properties }),
    });
    return contactId;
  }

  // Does not exist — create with product properties included
  const create = await fetch("https://api.hubapi.com/crm/v3/objects/contacts", {
    method: "POST",
    headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
    body: JSON.stringify({ properties: { email, ...properties } }),
  });
  const newContact = await create.json();
  return newContact.id;
}

When to use search+create vs upsert: batch/upsert with idProperty: "email" is simpler and handles both cases atomically in one API call. Use the search+create pattern only when you need the contact ID back synchronously before writing other properties, or when building a single-contact sync path (not a batch path).

7. Rate limit wiring

Wire the retry helper to read Retry-After from 429 responses and respect it. Do not implement a fixed delay — the Retry-After value is the only one HubSpot guarantees will not extend your suspension.

typescript
async function withRetry<T>(
  fn: () => Promise<Response>,
  maxAttempts = 4,
  baseDelayMs = 1_000,
): Promise<Response> {
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    const res = await fn();

    if (res.status !== 429 && res.status < 500) return res;
    if (attempt === maxAttempts) return res;

    const retryAfterSec = parseInt(res.headers.get("Retry-After") ?? "0", 10);
    const expDelay = Math.min(30_000, baseDelayMs * 2 ** attempt);
    const jittered = Math.random() * expDelay;
    const delay = retryAfterSec > 0 ? retryAfterSec * 1_000 : jittered;

    console.warn("HubSpot rate limit or server error", {
      status: res.status,
      attempt,
      delayMs: delay,
      dailyRemaining: res.headers.get("X-HubSpot-RateLimit-Daily-Remaining"),
    });

    await new Promise((r) => setTimeout(r, delay));
  }

  throw new Error("unreachable");
}

Error Handling

HTTP StatusErrorRoot CauseAction
200 OKNoneFull successProceed
207 Multi-StatusPer-object errors array in bodyPartial batch failure — some records failed, some succeededParse errors array; route failures to DLQ; do NOT retry the entire batch
400 BAD_REQUESTINVALID_PROPERTY_VALUEValue incompatible with property type (number to string, invalid enum option)Validate property type before write; fix upstream; do not retry as-is
400 BAD_REQUESTPROPERTY_DOESNT_EXISTProperty name is wrong or was deletedVerify property name via GET; re-run ensureProperty
400 BAD_REQUESTCONTACT_EXISTSCreate attempted with an email that already existsSwitch to update path; use upsert endpoint
401 UNAUTHORIZEDINVALID_AUTHENTICATIONToken expired or revokedRefresh or rotate token
403 FORBIDDENMISSING_SCOPESPrivate app missing crm.objects.contacts.writePortal admin re-grants scope
404 NOT_FOUNDOBJECT_NOT_FOUNDContact ID not found (on direct ID-based update)Switch to upsert by email; verify ID
409 CONFLICTDUPLICATE_VALUEDuplicate unique property valueDeduplicate before writing; use upsert
429 TOO_MANY_REQUESTSRATE_LIMITBurst or daily quota exhaustedRespect Retry-After header; reduce flush frequency; implement token-bucket
5xx SERVER_ERRORVariousHubSpot internal errorRetry with exponential backoff and jitter; DLQ after max attempts

207 is a success status code — do not treat it as success. A 207 from batch/update or batch/upsert means the request completed but individual records inside the batch may have failed. Always inspect body.errors.

Examples

Bootstrap: define your custom property schema
typescript
// Run once at service startup
const PRODUCT_PROPERTIES: PropertyDefinition[] = [
  {
    name: "hs_product_last_active_date",
    label: "Last Active Date (Product)",
    type: "datetime",
    fieldType: "date",
    groupName: "product_signals",
    description: "Last timestamp the contact triggered a product event",
  },
  {
    name: "hs_product_total_sessions",
    label: "Total Sessions (Product)",
    type: "number",
    fieldType: "number",
    groupName: "product_signals",
    description: "Lifetime session count from product backend",
  },
  {
    name: "hs_product_current_plan",
    label: "Current Plan (Product)",
    type: "enumeration",
    fieldType: "select",
    groupName: "product_signals",
    description: "Plan tier from billing system",
  },
];

for (const prop of PRODUCT_PROPERTIES) {
  await ensureProperty(token, "contacts", prop);
}
Sync a single product event
typescript
const event: ProductEvent = {
  eventId: "evt_01HZ9...",
  email: "alice@acme.com",
  properties: {
    hs_product_last_active_date: Date.now(),
    hs_product_total_sessions: 47,
    hs_product_current_plan: "pro",
  },
  occurredAt: Date.now(),
};

queue.push(event);
Replay DLQ events
bash
# Example: replay from a Postgres DLQ table
psql $DATABASE_URL -c "
  SELECT id, event_json FROM hubspot_dlq
  WHERE retry_count < 3
    AND next_retry_at < NOW()
  LIMIT 500
" --csv | python3 scripts/replay-dlq.py
Verify a batch write worked
bash
# Check the contact after a sync
curl -s \
  "https://api.hubapi.com/crm/v3/objects/contacts/search" \
  -H "Authorization: Bearer $HUBSPOT_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "filterGroups": [{"filters": [{"propertyName": "email", "operator": "EQ", "value": "alice@acme.com"}]}],
    "properties": ["email", "hs_product_last_active_date", "hs_product_total_sessions", "hs_product_current_plan"]
  }' | jq '.results[0].properties'
Check rate limit headroom before a bulk backfill
bash
curl -si \
  "https://api.hubapi.com/crm/v3/objects/contacts?limit=1" \
  -H "Authorization: Bearer $HUBSPOT_ACCESS_TOKEN" \
  | grep -i "x-hubspot-ratelimit"

Output

  • Property schema verified and created at startup — no runtime 400s from missing or type-mismatched properties
  • Event queue that batches 100 events or flushes every 5 seconds, whichever comes first
  • Idempotency keys stored in Redis with 48-hour TTL — safe to retry any event up to two days after first attempt
  • 207 Multi-Status parsed per-object — failures routed to DLQ, successes counted and logged
  • Dead-letter queue with retry count, error context, and replay path
  • Upsert-by-email on all batch writes — contacts auto-created on first event; no silent drops
  • Rate-limit retry with Retry-After header respected; exponential backoff with full jitter
  • Structured log output per batch: events sent, succeeded, failed, daily rate-limit remaining

Resources

© jeremylongshore, MIT. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file

Files

SKILL.md and 3 other files (references) in skills/.curated/hubspot-product-event-sync of jeremylongshore/tons-of-skills-marketplace.

  • SKILL.md
  • references/API_REFERENCE.md
  • references/event-queue-implementation.md
  • references/implementation-guide.md

Open the folder on GitHubat commit cfae287

Compare with similar skills

Hubspot Product Event Sync next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.

Hubspot Product Event Sync compared with similar skills
SkillStarsUsed inTokensAuto-checkLicenceRepo updated
Hubspot Product Event Sync this skilljeremylongshore/tons-of-skills-marketplace2.8k—~5.6kAutomated safety check: PassMIT
Google Maps Exportgmapsscraper/google-maps-agent-skills132—~1.2kAutomated safety check: PassMIT
Lost Deal Revival AgentOthmane-Khadri/YALC-the-GTM-operating-system318—~3kAutomated safety check: PassMIT
HubspotOpenClaudia/openclaudia-skills713—~1.8kAutomated safety check: NotesMIT
Aai Hubspotaai-labs/agent-barn109—~223Automated safety check: PassApache-2.0
Hubspotrefly-ai/refly-skills204—~491Automated safety check: PassNone

Similar skills

  • Google Maps Export

    gmapsscraper/google-maps-agent-skills

    Export Google Maps business data to CSV, JSON, or CRM format (HubSpot, Pipedrive, Salesforce).

    132 GitHub stars~1.2k tokensUpdated 4 mo ago
    Sales & SupportAuto-check passed
  • Lost Deal Revival Agent

    Othmane-Khadri/YALC-the-GTM-operating-system

    Drafts revival messages for closed-lost deals when a public company signal contradicts the original objection.

    318 GitHub stars~3k tokensUpdated 1 mo ago
    Sales & SupportAuto-check passed
  • Hubspot

    OpenClaudia/openclaudia-skills

    Manage HubSpot CRM contacts, companies, deals, and CMS content via API.

    713 GitHub stars~1.8k tokensUpdated 23 days ago
    Sales & SupportAuto-check: notes
  • Aai Hubspot

    aai-labs/agent-barn

    Use aai-cli to inspect HubSpot CRM records, files, events, conversations, visitor identification, and custom channels.

    109 GitHub stars~223 tokensUpdated yesterday
    Sales & SupportAuto-check passed
  • Hubspot

    refly-ai/refly-skills

    Integrate with HubSpot for CRM management. An agent skill from refly-ai/refly-skills.

    204 GitHub stars~491 tokensUpdated 2 mo ago
    Sales & SupportAuto-check passed
  • Hubspot

    Anil-matcha/awesome-muse-connectors

    Read and manage the HubSpot CRM: contacts, contact search, deals.

    1.3k GitHub stars~558 tokensUpdated 5 days ago
    Sales & SupportAuto-check passed

More from jeremylongshore/tons-of-skills-marketplace

All 3,342 skills in this repo
  • Performing Security Code Review

    jeremylongshore/tons-of-skills-marketplace

    Execute this skill enables AI assistant to conduct a security-focused code review using the security-agent plugin.

    2.8k GitHub starsUsed in 2 repos~1.3k tokens
    Auto-check: notes
  • Adapting Transfer Learning Models

    jeremylongshore/tons-of-skills-marketplace

    Build this skill automates the adaptation of pre-trained machine learning models using transfer learning techniques.

    2.8k GitHub stars~1.1k tokensUpdated today
    Auto-check passed
  • Agent Context Loader

    jeremylongshore/tons-of-skills-marketplace

    Execute proactive auto-loading: automatically detects and loads agents.md files.

    2.8k GitHub stars~1.1k tokensUpdated today
    Auto-check passed
  • Aggregating Performance Metrics

    jeremylongshore/tons-of-skills-marketplace

    Aggregate and centralize performance metrics from applications, systems, databases, caches, and services.

    2.8k GitHub stars~1.2k tokensUpdated today
    Auto-check passed
  • Analyzing Capacity Planning

    jeremylongshore/tons-of-skills-marketplace

    Execute this skill enables AI assistant to analyze capacity requirements and plan for future growth.

    2.8k GitHub stars~947 tokensUpdated today
    Auto-check passed
  • Analyzing Database Indexes

    jeremylongshore/tons-of-skills-marketplace

    Process use when you need to work with database indexing. An agent skill from jeremylongshore/tons-of-skills-marketplace.

    2.8k GitHub stars~2k tokensUpdated today
    Auto-check passed

Works with

Categories

Questions about Hubspot Product Event Sync

What does Hubspot Product Event Sync do?

Sync backend product events into HubSpot contact and company custom properties using idempotent batched updates — the Segment integration pattern without Segment. Hubspot Product Event Sync is an agent skill from jeremylongshore/tons-of-skills-marketplace. Sync backend product events into HubSpot contact and company custom properties using idempotent batched updates — the Segment integration pattern without Segment.

When should I use Hubspot Product Event Sync?

Hubspot Product Event Sync fits situations like: you need to push server-side behavioral signals (feature usage; last-seen timestamps; trial milestones) into HubSpot CRM properties so sales and marketing can act on product data without a CDP in the stack; with hubspot product events.

How do I install Hubspot Product Event Sync in Claude Code?

Run `npx skills add jeremylongshore/tons-of-skills-marketplace --skill hubspot-product-event-sync -a claude-code`. Or copy the skill folder (skills/.curated/hubspot-product-event-sync in jeremylongshore/tons-of-skills-marketplace) into .claude/skills/hubspot-product-event-sync in your project. Claude Code loads it when a task matches its description.

How do I install Hubspot Product Event Sync in Codex?

Run `npx skills add jeremylongshore/tons-of-skills-marketplace --skill hubspot-product-event-sync -a codex`. Or copy the skill folder (skills/.curated/hubspot-product-event-sync in jeremylongshore/tons-of-skills-marketplace) into .agents/skills/hubspot-product-event-sync in your project. Codex loads it when a task matches its description.

Can I use Hubspot Product Event Sync in Cursor, Gemini CLI or GitHub Copilot?

Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add jeremylongshore/tons-of-skills-marketplace --skill hubspot-product-event-sync -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/hubspot-product-event-sync, .gemini/skills/hubspot-product-event-sync, .github/skills/hubspot-product-event-sync and .opencode/skills/hubspot-product-event-sync in your project.

What does Hubspot Product Event Sync need to run?

Going by SKILL.md and its folder, Hubspot Product Event Sync needs the command-line tools its instructions call (curl, psql, python3 and jq) and credentials named HUBSPOT_ACCESS_TOKEN. Our summary lists: Python 3; Node.js. Its frontmatter pre-approves these tools: Read, Write, Edit, Bash(curl:*), Bash(jq:*), Bash(python3:*), Grep. Compatibility (from SKILL.md): Designed for Claude Code.

Does Hubspot Product Event Sync access the network?

SKILL.md names 2 domains. In commands or code: api.hubapi.com; the agent is likely to contact it when it follows the instructions. As links in the text: developers.hubspot.com. This is read from the text; nothing was executed.

Is Hubspot Product Event Sync safe to install?

Our automated static check of SKILL.md found no risky patterns, such as piping downloads into a shell, reading credential files or hidden Unicode. It is not a guarantee. Review the folder before installing.

What licence does Hubspot Product Event Sync use?

Hubspot Product Event Sync is published under the MIT licence (declared in SKILL.md). It allows redistribution, so the full SKILL.md is shown on this page.

How many tokens does Hubspot Product Event Sync use?

About 5.6k tokens (SKILL.md is roughly 22k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full. Its references folder adds about 10k tokens, read only when the agent opens those files.

What are the alternatives to Hubspot Product Event Sync?

Skills that share tags, products or a category with Hubspot Product Event Sync: Google Maps Export (gmapsscraper/google-maps-agent-skills, 132 stars), Lost Deal Revival Agent (Othmane-Khadri/YALC-the-GTM-operating-system, 318 stars), Hubspot (OpenClaudia/openclaudia-skills, 713 stars) and Aai Hubspot (aai-labs/agent-barn, 109 stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.

Who maintains Hubspot Product Event Sync?

jeremylongshore (a GitHub user) maintains it in jeremylongshore/tons-of-skills-marketplace, which has 2,827 GitHub stars. The repository holds 3,342 skills in this directory. The repository was last updated on October 10, 2026.

Source: jeremylongshore/tons-of-skills-marketplace on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.