Skip to content

New package: @relayfile/adapter-hubspot (pilot — target 2026-04-21) #17

@khaliqgant

Description

@khaliqgant

Summary

Add a new `@relayfile/adapter-hubspot` package that maps HubSpot data (delivered via NangoHQ/integration-templates syncs) into the relayfile VFS, so agents can read HubSpot contacts, companies, and engagements as files with no HubSpot API knowledge.

This is the upstream blocker for a Sage design-partner pilot. Full context and the three-layer architecture live in:

Target demo date: 2026-04-21. The Sage-side HubSpot tool PR depends on this package being mergeable (or link-installed in the monorepo) by 2026-04-17. A fallback `NangoHubSpotClient` shim exists in Sage for the worst case, but the clean path is this adapter.

Why

For Sage to deliver that without writing a custom HubSpot client, HubSpot needs to be a first-class relayfile provider alongside `/github`, `/notion`, `/linear`, `/slack`.

Scope

Package layout

Scaffold off `packages/notion` — closest analogue: record-shaped data with associations, no PR/commit semantics.

```
packages/hubspot/
├── package.json # @relayfile/adapter-hubspot
├── tsconfig.json
├── hubspot.mapping.yaml # adapter-core mapping (optional seed)
├── src/
│ ├── index.ts
│ ├── adapter.ts # HubSpotAdapter extends IntegrationAdapter
│ ├── client.ts # Thin NangoProvider wrapper
│ ├── path-mapper.ts
│ ├── types.ts # HubSpotContact, HubSpotCompany, HubSpotEngagement
│ ├── contacts/ingestion.ts
│ ├── companies/ingestion.ts
│ ├── engagements/ingestion.ts
│ ├── webhook/normalize.ts # Nango sync webhook → WebhookInput
│ ├── sync.ts # Bootstrap via Nango /records endpoint
│ └── tests/
│ ├── path-mapper.test.ts
│ ├── normalize.test.ts
│ └── ingestion.test.ts
└── workflows/
└── bootstrap-from-nango.ts
```

VFS path scheme

```
/hubspot/
├── index.json # Known contact ids + last sync ts
├── contacts/
│ └── {contactId}/
│ ├── metadata.json # Flattened HubSpot properties
│ ├── company.json # Associated company snapshot
│ └── engagements/
│ ├── index.json # Ordered engagement ids
│ └── {engagementId}.json
├── companies/
│ └── {companyId}/
│ └── metadata.json
└── views/
└── top-customers.json # Pre-computed top-N query result
```

`views/top-customers.json` is the load-bearing optimization: Sage's "top 5 customers" tool becomes a single file read instead of a filesystem walk or a runtime sort. Regenerated on every sync from the `lifecyclestage=customer` slice of contacts, ordered by a configurable sort property (default `hs_lastmodifieddate`).

Upstream data source (Nango)

We deliberately write zero HubSpot API code in this package for reads. Data arrives via NangoHQ/integration-templates:

  • `syncs/contacts.ts` — scheduled incremental sync, emits per-record webhooks
  • `syncs/companies.ts` — same
  • `actions/search-companies.ts`, `actions/get-contact.ts`, `actions/list-contacts.ts` — on-demand lookups for the Sage fallback path and the top-customers view materialization

Engagements gap. Nango templates do not ship an engagements sync. For the pilot we handle this with a 30-line custom action (`list-contact-engagements`) deployed to our Nango workspace that calls `GET /crm/v3/objects/contacts/{id}/associations/engagements` + batch `GET /crm/v3/objects/engagements/{id}`. The adapter consumes the action output the same way it consumes sync webhooks. Candidate for upstreaming to `integration-templates` post-pilot.

Adapter interface

Implements `IntegrationAdapter` from `@relayfile/sdk`, matching the shape already used by `NotionAdapter`:

```ts
export class HubSpotAdapter extends IntegrationAdapter {
readonly name = 'hubspot';
readonly version = '0.1.0';

computePath(objectType: string, objectId: string, context?: Record<string, string>): string;
computeSemantics(objectType: string, objectId: string, payload: Record<string, unknown>): FileSemantics;

async ingestWebhook(workspaceId: string, event: WebhookInput): Promise;
async sync(workspaceId: string, options?: SyncOptions): Promise;
// writeBack: deferred to a later slice; pilot is read-only
}
```

Webhook normalization

Map Nango sync events (not HubSpot webhooks directly):

  • `model: Contact` + `action: added|updated` → `objectType: 'contact'`, path = `/hubspot/contacts/{id}/metadata.json`
  • `model: Company` + `action: added|updated` → `objectType: 'company'`, path = `/hubspot/companies/{id}/metadata.json`
  • Custom `list-contact-engagements` action result → `objectType: 'engagement'`, path = `/hubspot/contacts/{contactId}/engagements/{id}.json`
  • Any sync completion → regenerate `/hubspot/views/top-customers.json`

Registration

Add `HubSpotAdapter` to the adapter registry in `packages/webhook-server` so `/webhooks/nango` routes `provider: hubspot` events to it.

Optional: adapter-core scaffold

HubSpot publishes an OpenAPI spec. Running `npx adapter-core generate --spec hubspot-openapi.yaml --mapping hubspot.mapping.yaml --output ./src` can seed types and writeback rules. Writeback is deferred; the generated scaffold stays read-only for the pilot. Up to the implementer whether to seed from adapter-core or hand-write given the narrow object surface (contacts, companies, engagements only).

Acceptance criteria

  • New package `packages/hubspot/` builds + tests green under `npx turbo build` / `npx turbo test`
  • `HubSpotAdapter.computePath` produces the VFS paths listed above for contact, company, and engagement object types (unit test)
  • `HubSpotAdapter.ingestWebhook` writes files to the VFS at the correct paths given fixture Nango webhook payloads (unit test)
  • `HubSpotAdapter.sync` bootstraps `/hubspot/contacts/`, `/hubspot/companies/`, and `/hubspot/views/top-customers.json` end-to-end against a staging relayfile workspace (integration smoke)
  • `/hubspot/views/top-customers.json` is regenerated on every sync completion and respects a configurable `sortProperty` (default `hs_lastmodifieddate`)
  • `HubSpotAdapter` registered in `packages/webhook-server` adapter registry
  • `workflows/bootstrap-from-nango.ts` runnable against a seeded Nango HubSpot sandbox

Out of scope for this package version

  • Writeback to HubSpot (create/update contacts, log engagements). Interface slot reserved; ships read-only.
  • Deals, tickets, tasks, marketing-emails, products, owners, service-tickets — Nango templates exist but we are not enabling them for the pilot.
  • Native engagements sync — consumed via the custom Nango action above; upstreaming to `integration-templates` is a separate follow-up.
  • Per-user HubSpot connections — pilot uses one workspace-level connection.
  • Custom-property schema introspection — the adapter stores whatever properties the Nango sync templates surface by default.

Timeline (from the Sage pilot tracking issue)

Date Milestone
2026-04-15 This issue opened; scaffold PR opened against `relayfile-adapters`
2026-04-17 PR merged or link-installed in the monorepo so Sage can import `@relayfile/adapter-hubspot`
2026-04-19 Sage-side `src/swarm/hubspot-tool.ts` lands on top of this package
2026-04-21 Demo to Stephan

Related

  • `AgentWorkforce/sage#17` — pilot specs (including the full spec 01 for this work)
  • `AgentWorkforce/sage#18` — pilot tracking issue with the end-to-end checklist

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions