From estuary-schema
Creates a HubSpot Real-Time capture with flowctl to stream CRM data (contacts, companies, deals, tickets) into Estuary collections. Use when setting up a HubSpot source.
How this skill is triggered — by the user, by Claude, or both
Slash command
/estuary-schema:capture-hubspot-createThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Create a HubSpot capture using flowctl to stream CRM data from HubSpot into Estuary collections in real time.
Create a HubSpot capture using flowctl to stream CRM data from HubSpot into Estuary collections in real time.
Applies to: source-hubspot-native (HubSpot Real-Time connector)
Before proceeding, fetch the official connector docs for prerequisites, config reference, and OAuth setup.
Load the docs page: https://docs.estuary.dev/reference/Connectors/capture-connectors/HubSpot-real-time/
Use WebFetch to load this page. It covers:
useLegacyNamingForCustomObjects)Search Kapa for tribal knowledge (if the Estuary MCP is configured):
Search kapa ai knowledge sources for "capture hubspot common issues"
If Kapa MCP is not configured, the user can set it up: https://docs.estuary.dev/features/mcp-integration/
This skill provides the flowctl workflow and troubleshooting that docs don't cover.
Before writing any YAML, ask the user:
HubSpot account access? — User must have access to the HubSpot account being captured
OAuth credentials? — The connector authenticates with HubSpot via OAuth using client_id + client_secret + refresh_token. The easiest path is to complete the OAuth flow in the Estuary web UI (it mints the refresh_token for you), then pull the spec to local. Building your own HubSpot OAuth app and exchanging an auth code for a refresh token manually is supported but more involved.
The connector's JSON schema also exposes a Private App Credentials discriminator, but in practice HubSpot Private App tokens have not been observed to authenticate successfully against the connector — use OAuth.
Non-default data plane? — Most users use the default. Ask if they need a non-default data plane.
Which resources to capture? — All discovered resources (default) or a subset. The connector auto-discovers: Campaigns, Companies, Contact List Memberships, Contact Lists, Contacts, Custom Objects, Deal Pipelines, Deals, Email Events, Engagements, Feedback Submissions, Form Submissions, Forms, Goals, Line Items, Marketing Emails, Marketing Events, Orders, Owners, Products, Properties, Tickets, Workflows.
Capture property history? — Off by default. Enable to include historical changes to HubSpot object properties in captured documents.
Calculated property refresh schedule? — Per-binding cron. The web UI pre-fills 55 23 * * * (daily at 23:55 UTC), but the connector's spec default is "" (disabled) — set the cron explicitly if you want refreshes. See note below.
Custom objects naming? — Off by default. The hidden useLegacyNamingForCustomObjects field is false by default, which prefixes custom object bindings with custom_ (e.g., custom_form_submissions) to avoid collisions with standard objects. Do not change without contacting Estuary Support — it affects discovered resource and collection names.
During the OAuth flow, HubSpot may present scopes that include create/update/delete permissions. This is due to how HubSpot groups permissions and exposes some read APIs behind combined read/write scopes.
The connector is read-only. It only reads data from HubSpot and never creates, updates, or deletes CRM objects or other resources. Reassure the user before they click "Approve".
HubSpot calculated properties are evaluated at query time and do not update the record's updatedAt timestamp when they change. The connector uses updatedAt for incremental change detection, so calculated property updates are missed by the normal real-time path.
To work around this, the connector can refresh calculated property values on a cron schedule (per-binding schedule field). When a refresh fires, it fetches every record's current calculated property values and merges them in.
null. Warn the user.Always use the latest numbered version tag. Query the connector registry:
flowctl raw get --table connector_tags \
--query 'documentation_url=eq.https://go.estuary.dev/hubspot-real-time' \
--query 'select=image_tag,documentation_url' \
--output yaml
Use the returned image_tag — never hardcode a version.
The OAuth Authorization Code flow needs an interactive browser redirect, so it cannot be completed inside a headless agent / CLI session. The user must complete it elsewhere first, then bring the resulting credentials back to flowctl.
refresh_tokenflowctl catalog pull-specs --name <TENANT>/<PATH>/source-hubspot-native
If the user needs to manage their own OAuth app (for an air-gapped tenant, scope control, etc.):
hs project create (HubSpot CLI, v7.6.0+) or the HubSpot Developer portalrequiredScopes / optionalScopes and redirectUrls on the apprefresh_token against https://api.hubapi.com/oauth/v1/tokenclient_id, client_secret, and refresh_token into the specA small local helper (HTTP server on the registered redirect URI + curl for the token exchange) is the most reliable way to drive the code-to-refresh-token step from a CLI workflow. flowctl raw oauth exists for this purpose but currently has a parsing bug (see Troubleshooting).
Build flow.yaml using the config reference from the docs:
captures:
<TENANT>/<PATH>/source-hubspot-native:
endpoint:
connector:
image: ghcr.io/estuary/source-hubspot-native:<VERSION>
config:
capturePropertyHistory: false
credentials:
credentials_title: "OAuth Credentials"
client_id: "<CLIENT_ID>"
client_secret: "<CLIENT_SECRET>"
refresh_token: "<REFRESH_TOKEN>"
# useLegacyNamingForCustomObjects: false # hidden — do not change without Estuary Support
bindings: []
Important:
credentials_title must be the literal string "OAuth Credentials" — it's the discriminator the connector uses to pick the auth schema.client_id, client_secret, and refresh_token are required.useLegacyNamingForCustomObjects is hidden in the dashboard and only editable via flowctl. Leave at default unless instructed by Estuary Support.client_secret and refresh_token are secrets — don't commit them in plain text. Encrypt them with Estuary's sops-based mechanism:
# Encrypt only the secret fields in the spec (sops + KMS)
sops --encrypt \
--input-type yaml --output-type yaml \
--encrypted-suffix "_token" \
--encrypted-suffix "_secret" \
--gcp-kms projects/<PROJECT>/locations/global/keyRings/<RING>/cryptoKeys/<KEY> \
flow.yaml > flow.encrypted.yaml
mv flow.encrypted.yaml flow.yaml
flowctl recognizes sops-encrypted specs and decrypts them at publish time. See https://docs.estuary.dev/concepts/connectors/#protecting-secrets for full options (AWS KMS / Azure Key Vault / age / etc.).
# Discover available HubSpot resources (auto-generates bindings)
flowctl discover --source flow.yaml
# Review the generated bindings — remove any resources the user doesn't want
cat flow.yaml
# Optionally adjust the per-binding calculated property refresh schedule
# (default is "55 23 * * *", set to "" to disable)
# Publish the capture
flowctl catalog publish --source flow.yaml --auto-approve
Example binding with explicit refresh schedule:
bindings:
- resource:
name: companies
schedule: "55 23 * * *" # daily at 23:55 UTC
target: <TENANT>/<PATH>/companies
- resource:
name: contacts
schedule: "" # disable calculated property refresh (connector default)
target: <TENANT>/<PATH>/contacts
# Check status
flowctl catalog status <TENANT>/<PATH>/source-hubspot-native
# View logs
flowctl logs --task <TENANT>/<PATH>/source-hubspot-native --since 5m | jq -c '{ts, message}'
# Read captured data
flowctl collections read --collection <TENANT>/<PATH>/<resource> --uncommitted | head -10
Status progression:
PENDING — Normal for ~30 seconds during shard assignmentBACKFILLING — Initial backfill of each discovered resourceOK — Running normally with real-time updates and scheduled calculated-property refreshesCause: Refresh token was revoked, expired, or copied incorrectly. HubSpot refresh tokens can be revoked if the connected app is uninstalled or the user's HubSpot session is invalidated.
Fix:
refresh_tokenBAD_CLIENT_ID / "missing or unknown client id" (HTTP 400 from https://api.hubapi.com/oauth/v1/token)Cause: The client_id (or client_secret) under credentials is wrong, a placeholder, or for a HubSpot OAuth app that has been deleted.
Fix:
client_id and client_secret match a current HubSpot OAuth app.EXPIRED_AUTHENTICATION with expire time: 1970-01-01T00:00:00ZCause: A non-OAuth bearer token was supplied (e.g. a HubSpot Private App access token, or a CMS API key) under what the connector treats as an OAuth grant. HubSpot recognizes the token shape but cannot decode an expiry from it, so it falls back to the Unix epoch — making the error message say "expired 20602 day(s) ago." Despite the JSON schema exposing a Private App Credentials discriminator, in practice only OAuth Credentials reliably authenticates.
Fix: Use credentials_title: "OAuth Credentials" with a real client_id / client_secret / refresh_token (see Step 3).
Cause: HubSpot groups some read APIs behind combined read/write scopes. This is HubSpot's permission model, not the connector requesting write access.
Fix: This is expected. The connector is read-only — it never creates, updates, or deletes data. Approve the scopes to proceed.
Cause: useLegacyNamingForCustomObjects: true was set, allowing custom objects to shadow standard objects (e.g., a custom form_submissions object replaces the standard Form Submissions resource).
Fix: Set useLegacyNamingForCustomObjects: false (default). Custom objects will be prefixed with custom_ (e.g., custom_form_submissions). Contact Estuary Support before flipping this — it changes discovered resource names.
Cause: HubSpot calculated properties don't update updatedAt, so the incremental path misses them. They only refresh on the per-binding cron schedule.
Fix:
schedule — empty string disables refresh entirely55 23 * * * fires once a day at 23:55 UTC. Tighten the cron if you need fresher values (mindful of HubSpot rate limits)null in delta updates destinationsCause: Calculated property refreshes emit partial documents (only the key and calculated properties). Standard materializations merge these with the prior complete document; delta-update materializations don't reduce, so non-calculated fields land as null.
Fix: Use a standard (non-delta) materialization for tables that depend on calculated property values, or filter out the partial refresh documents in a derivation before the delta-update materialization.
Cause: HubSpot enforces API rate limits per portal. Tight calculated-property refresh schedules or very large portals can hit them.
Fix:
schedule (don't refresh every minute on a large portal)Cause: The OAuth token's HubSpot user doesn't have access to the missing resources, or the HubSpot account doesn't have the feature enabled (e.g., custom objects require certain HubSpot tiers).
Fix:
flowctl discover --source flow.yamlCause: The connector runs two incremental subtasks per resource — a realtime cursor (minutes-fresh) and a delayed cursor lagging by ~1 hour to pick up rows HubSpot's API may emit late. For some resources or bulk-write scenarios, fresh rows can sit on the delayed cursor side until the lag elapses.
Fix for testing or interactive demos: Bump the backfill counter on the affected bindings and republish — Estuary re-reads via the realtime path and downstream materializations see the new state in seconds.
bindings:
- resource: { name: contacts }
target: <TENANT>/<PATH>/contacts
backfill: 1 # increment each time you want a re-pull
flowctl catalog publish --source flow.yaml --auto-approve
flowctl raw oauth fails with "Type mismatch: expected a object"Cause: A current parsing bug in flowctl raw oauth — the --endpoint-config argument fails JSON-Schema validation for any input ({}, JSON, YAML all fail with the same error). The command can't currently drive a HubSpot OAuth flow locally.
Fix:
refresh_token for you), then pull the published spec down to local with flowctl catalog pull-specs.curl. Use curl rather than Python's urllib.request — the python.org macOS Python distribution doesn't link the system trust store and urlopen fails the TLS handshake.Wait 30-60 seconds — this is normal during shard assignment. If still stuck:
flowctl logs --task <TENANT>/<PATH>/source-hubspot-native --since 5m | jq 'select(.level == "error")'
Cause: capturePropertyHistory is false by default.
Fix: Set capturePropertyHistory: true in the endpoint config and republish. Note this increases document size and capture volume.
estuary-connector-restart — Pause/restart existing capturesestuary-logs — Deep log analysisestuary-catalog-status — Status checkingnpx claudepluginhub estuary/agent-skills --plugin estuary-schemaGuides collaborative design exploration before implementation: explores context, asks clarifying questions, proposes approaches, and writes a design doc for user approval.
Creates structured, bite-sized implementation plans from specs or requirements before writing code. Useful for breaking down multi-step tasks into testable steps with file structure and task boundaries.
Resolves in-progress git merge or rebase conflicts by analyzing history, understanding intent, and preserving both changes where possible. Runs automated checks after resolution.