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
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
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:
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):
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
Out of scope for this package version
Timeline (from the Sage pilot tracking issue)
Related