← Files Revenue AnalyticsARCHIVED FILE
skills/revenue-analytics/reference/schema-reference.md
7.54 KB · Oct 2, 2026 · 00:27 UTC
# HubSpot-Synced Schema Reference Your database is a relational mirror of your HubSpot portal, synced by DataLabs. **Portal schemas vary** - which tables exist, and the exact names of custom properties, depend on what's enabled/customized on your specific HubSpot account. Treat everything below as a map of *commonly-present* objects to orient yourself faster, never as a substitute for checking the live schema. Always confirm exact table/column names with `get_tables_list`, `get_database_schema_subtree`, or `get_object_relationships` before writing a query that references them - an invented column name fails loudly (the reader role and SQL parser reject unknown identifiers), so there's no silent-wrong-answer risk, but it wastes a round trip. ## Discovery tool order (cheapest first) 1. `get_tables_list` - every table with a one-line business-meaning description. Start here for "what's even in this database." 2. `get_semantic_metadata` - business terminology and categorical-field allowed values contributed by you or DataLabs. Check this before assuming what a code/enum column means. 3. `get_database_schema_subtree(table_name)` - the table you care about plus everything it's related to (columns, types, FKs, sample values). Use this for "I'm about to query Deal and friends" - it's scoped, so it's cheap. 4. `get_object_relationships(table_name?)` - FK cardinality without full column detail. Useful to sanity-check a join before writing it. 5. `get_data_statistics(table_name)` - row counts, null %, cardinality, common value distributions. Use before filtering on a column you haven't inspected, especially before assuming a boolean-like or categorical column's shape. 6. `get_full_database_schema` - **last resort only**. The tool's own description warns it returns large content; it dumps every table's full detail at once. Reach for the scoped alternatives above first. ## Core CRM objects | Table | What it holds | Notes | |---|---|---| | `Deal` | The sales-pipeline object | `dealstage` + `pipeline` together identify the current stage (see `PipelineDeal` below). `amount` is decimal/money - see SQL dialect notes. `hs_is_closed`, `hs_is_closed_won` are HubSpot's boolean-like flags **stored as the strings `'true'`/`'false'`**, not SQL `BIT`. `OwnerID` FKs to `Owner`. `createdate` is the deal's creation timestamp. | | `Contact` | People | Standard + custom contact properties. | | `Company` | Organizations | `numberofemployees` is commonly populated. Custom properties (e.g. an ARR-style field) exist under whatever name the portal defined - names are **not** standardized across portals; check the schema before assuming one. | | `Owner` | HubSpot users assigned to records | `firstName`/`lastName`; a Deal/Contact/Company with no owner has `OwnerID = NULL` - use `LEFT JOIN` and `ISNULL(... , '(unassigned)')`-style handling, not an inner join. | | `Ticket`, `Lead` | Support/lead objects | Same sync pattern as Deal/Contact/Company; check `get_tables_list` for the exact set enabled on this portal. | ## Pipeline & stage-history objects | Table | What it holds | Notes | |---|---|---| | `PipelineDeal` | Stage **definitions** (not deal instances, despite the name) | Columns include `ID`, `Pipeline`, `Label`, `PipelineLabel`, `Probability` (HubSpot's configured win probability for the stage), `IsClosed`, `DisplayOrder`. Join to `Deal` via `p.ID = d.dealstage AND p.Pipeline = d.pipeline` - **both** columns are needed, since stage IDs are only unique within a pipeline. | | `Deal_Stage` | Stage-transition **history** - when each deal entered/exited each stage it ever passed through | Columns include `DealID`, `PipelineStageID`, `hs_v2_date_entered`, `hs_v2_date_exited`. A deal's *current* stage row has `hs_v2_date_exited IS NULL`. This is the only place time-in-stage for **open** deals lives - HubSpot's own reporting only computes time-in-stage after a deal moves or closes, so this table is the wedge for pipeline-hygiene analysis. | ## Engagement & association objects | Table | What it holds | Notes | |---|---|---| | `Engagement` | Logged activity (calls, emails, meetings, notes, tasks, communications, postal mail) | A separate object from `Deal`/`Contact` - reach it only through a bridge table. | | `DealEngagements` | Bridge: which engagements are logged against which deal | One row per (Deal, Engagement) pair. `COUNT(*) GROUP BY DealID` gives a deal's engagement count without needing to join `Engagement` itself if you only need the count. | | `ContactDeals` | Bridge: which contacts are associated with which deals | Carries a `Name` column labeling the association type. In at least one portal, both `'CONTACT_TO_DEAL'` and a `'..._UNLABELED'` variant exist for what's conceptually the same association - filter to the labeled one (`WHERE cd.Name = 'CONTACT_TO_DEAL'`) to avoid double-counting; verify the exact label set on your portal with `get_data_statistics` or a quick `SELECT DISTINCT Name`. | ## Marketing / email objects | Table | What it holds | Notes | |---|---|---| | `EmailCampaign` | One row per email **send** | `Name`, `Subject`. Some portals also have a separate `MarketingEmail` (the authored asset, linked via `MarketingEmail.PrimaryEmailCampaignId`) - others don't; if `MarketingEmail` isn't in `get_tables_list`, read `Name`/`Subject` directly off `EmailCampaign`. | | `EmailCampaignEvent` | One row per recipient-level interaction (`OPEN`, `CLICK`, `DELIVERED`, `SENT`, `BOUNCE`, `DROPPED`, `STATUSCHANGE`, `SPAMREPORT`, ...) | **The highest-cardinality table in a synced portal** - every send × every recipient × every interaction. Can run into the millions of rows. `ContactID` is nullable (recipient didn't match a known Contact) - filter `WHERE ContactID IS NOT NULL` before joining to Contact-scoped analysis. `Created` is a real `DateTime`. Never pull this table client-side to join by hand; push every join into the SQL query itself (see the SQL dialect notes on row limits). | ## Workflow (automation) objects | Table | What it holds | Notes | |---|---|---| | `Workflow` | A workflow **blueprint** (trigger + action graph definition) | Not a per-run instance - HubSpot has no queryable "workflow run" object. A record *enrolling* in a workflow is state on the record, not a new row here. | | `WorkflowAction` | The action graph, exploded into rows | One row per action step (`actionId`, `actionTypeId`, connection to next action). | | `WorkflowPerformance` | Aggregate enrollment/completion counts | Time-bucketed by `Granularity` (DAY/WEEK/MONTH) + `StartDate`. This is where "how well is this workflow performing" lives - not per-record enrollment detail. | | `WorkflowEmailCampaign` | Bridge: which `EmailCampaign`/`MarketingEmail` rows a workflow references | | ## Commerce objects (sparse on many portals) `Subscription`, `Payment`, `Discount`, `LineItem` exist in the schema but are frequently near-empty on portals that don't run HubSpot Commerce - always check `get_data_statistics` (row count) before building an analysis that depends on them, and say so plainly if the data isn't there rather than presenting a technically-correct-but-empty result as if it were a finding. ## Custom properties Any object can carry customer-defined custom properties in addition to the standard fields listed above. Their names follow whatever convention the portal's HubSpot admin chose - there is no universal naming rule to rely on (in particular, don't assume a Salesforce-style `__c` suffix or any other fixed pattern). Discover them the same way as everything else: `get_database_schema_subtree`/`get_full_database_schema` for the object, and `get_semantic_metadata` for what a cryptically-named one actually means.
SHA-256: 3d59922574265d08a3bc483f558e42395b9d43f5838993665c9d41e082f489ee