Skip to content

Commit 105d786

Browse files
committed
feat(harmonic): add the missing people endpoints and fix two error paths
Extends the integration from 4 to 13 tools, covering every non-deprecated people-scoped Harmonic endpoint, and repairs two defects found by validating the existing tools against Harmonic's OpenAPI and API reference. New tools: - Enrich Person (POST /persons) — the only path from a LinkedIn URL or email a workflow already holds to a Harmonic contact. - Get Person, Get Company Employees — account-based sourcing; employees returns URNs that chain into Batch Get People. - Saved-search net-new results and their acknowledgement, so a monitor stops reprocessing the entire result set on every poll. - Bulk email enrichment: submit, poll, and quota, plus Get Enrichment Status. Fixes: - The error extractor dropped Harmonic's string and object `detail` envelopes. A tool that names an extractor gets no fallback chain, so every FastAPI abort surfaced as "Request failed with status 403". The enrichment 404 also carries the scheduled `enrichment_urn`, which was being discarded — that URN is the only handle on the job, so it is now kept in the message. - The saved-search selector failed the whole dropdown instead of degrading: the response cap was half the sibling value on an endpoint that is unpaginated and returns every saved search with its full query object, and the option ceiling threw rather than truncating. Raised to 1MB and switched to truncate-and-warn, matching the other data-driven selectors. Clearing net-new results now requires an explicit scope. Harmonic treats an absent `entity_urns` as "clear everything", so an empty field would have silently discarded the backlog. Scope deliberately excludes company-side, deal, typeahead, network, and Scout streaming endpoints, and every endpoint retiring on 2026-11-05.
1 parent ebd2d56 commit 105d786

25 files changed

Lines changed: 2887 additions & 66 deletions

apps/docs/content/docs/en/integrations/harmonic.mdx

Lines changed: 273 additions & 8 deletions
Large diffs are not rendered by default.

apps/sim/app/api/tools/harmonic/saved-searches/route.test.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,7 @@ describe('POST /api/tools/harmonic/saved-searches', () => {
293293
mockFetch
294294
.mockResolvedValueOnce(providerResponse('{not-json'))
295295
.mockResolvedValueOnce(
296-
providerResponse('[]', 200, { 'content-length': String(512 * 1024 + 1) })
296+
providerResponse('[]', 200, { 'content-length': String(1024 * 1024 + 1) })
297297
)
298298

299299
for (let requestNumber = 0; requestNumber < 2; requestNumber++) {
@@ -324,18 +324,20 @@ describe('POST /api/tools/harmonic/saved-searches', () => {
324324
expect(body.savedSearches).toHaveLength(HARMONIC_SAVED_SEARCH_SELECTOR_MAX_OPTIONS)
325325
})
326326

327-
it('fails instead of silently truncating more than the option ceiling', async () => {
327+
it('truncates to the option ceiling instead of failing the whole selector', async () => {
328328
mockFetch.mockResolvedValueOnce(
329329
providerResponse(
330-
Array.from({ length: HARMONIC_SAVED_SEARCH_SELECTOR_MAX_OPTIONS + 1 }, (_, index) =>
330+
Array.from({ length: HARMONIC_SAVED_SEARCH_SELECTOR_MAX_OPTIONS + 25 }, (_, index) =>
331331
peopleSearch(index + 1)
332332
)
333333
)
334334
)
335335

336336
const response = await POST(request(REQUEST_BODY), {})
337+
const body = await json(response)
337338

338-
expect(response.status).toBe(502)
339+
expect(response.status).toBe(200)
340+
expect(body.savedSearches).toHaveLength(HARMONIC_SAVED_SEARCH_SELECTOR_MAX_OPTIONS)
339341
})
340342

341343
it.each([

apps/sim/app/api/tools/harmonic/saved-searches/route.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export const dynamic = 'force-dynamic'
2222
const logger = createLogger('HarmonicSavedSearchesAPI')
2323
const HARMONIC_SAVED_SEARCHES_URL = 'https://api.harmonic.ai/savedSearches'
2424
const SELECTOR_REQUEST_MAX_BYTES = 8 * 1024
25-
const PROVIDER_RESPONSE_MAX_BYTES = 512 * 1024
25+
const PROVIDER_RESPONSE_MAX_BYTES = 1024 * 1024
2626
const PROVIDER_RESPONSE_MAX_ROWS = 2_000
2727
const PROVIDER_FETCH_TIMEOUT_MS = 10_000
2828

@@ -116,7 +116,16 @@ function normalizeSavedSearches(value: unknown): SavedSearchOption[] {
116116
}
117117
if (existingByUrn) continue
118118
if (byUrn.size >= HARMONIC_SAVED_SEARCH_SELECTOR_MAX_OPTIONS) {
119-
throw new Error('Harmonic returned too many people saved searches')
119+
/**
120+
* `GET /savedSearches` is unpaginated, so this ceiling bounds customer data
121+
* rather than a provider catalog. Every sibling selector with a data-driven
122+
* bound truncates and warns; failing here would leave the dropdown dead with
123+
* no in-place recovery.
124+
*/
125+
logger.warn('Harmonic saved-search list hit the option ceiling; list may be incomplete', {
126+
cap: HARMONIC_SAVED_SEARCH_SELECTOR_MAX_OPTIONS,
127+
})
128+
break
120129
}
121130
byUrn.set(option.urn, option)
122131
urnById.set(option.id, option.urn)

apps/sim/blocks/blocks/harmonic.test.ts

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,18 @@ describe('HarmonicBlock', () => {
2121
it('maps every dropdown operation onto exactly one registered tool', () => {
2222
expect(operationIds).toEqual([
2323
'harmonic_search_people_scout',
24+
'harmonic_enrich_person',
25+
'harmonic_get_person',
26+
'harmonic_batch_get_people',
27+
'harmonic_get_company_employees',
2428
'harmonic_list_people_saved_searches',
2529
'harmonic_get_people_saved_search_results',
26-
'harmonic_batch_get_people',
30+
'harmonic_get_people_saved_search_net_new_results',
31+
'harmonic_clear_people_saved_search_net_new_results',
32+
'harmonic_submit_email_enrichment_job',
33+
'harmonic_get_email_enrichment_job',
34+
'harmonic_get_email_enrichment_usage',
35+
'harmonic_get_enrichment_status',
2736
])
2837
expect(operationIds.map((id) => selectTool({ operation: id }))).toEqual(operationIds)
2938
expect(new Set(operationIds)).toEqual(new Set(HarmonicBlock.tools.access))
@@ -133,11 +142,39 @@ describe('HarmonicBlock', () => {
133142
description: 'Reusable Harmonic team API-key credential',
134143
},
135144
query: { type: 'string', description: 'Natural-language Harmonic Scout people query' },
145+
linkedinUrl: { type: 'string', description: 'LinkedIn profile URL to enrich' },
146+
email: { type: 'string', description: 'Email address used as an enrichment fallback' },
147+
personId: { type: 'string', description: 'Harmonic person ID or full person URN' },
148+
companyContextUrns: {
149+
type: 'array',
150+
description: 'Company URNs scoping the returned experience context',
151+
},
152+
companyId: { type: 'string', description: 'Harmonic company ID or full company URN' },
153+
employeeGroupType: { type: 'string', description: 'Employee role group filter' },
154+
employeeStatus: { type: 'string', description: 'Employment status filter' },
155+
userConnectionStatus: { type: 'string', description: 'Team or user connection filter' },
136156
savedSearchId: { type: 'string', description: 'People saved-search ID or full URN' },
157+
newResultsSince: {
158+
type: 'string',
159+
description: 'UTC cutoff for net-new saved-search matches',
160+
},
137161
personIds: { type: 'array', description: 'Numeric Harmonic person IDs to retrieve' },
138-
personUrns: { type: 'array', description: 'Harmonic person URNs to retrieve' },
139-
size: { type: 'number', description: 'Saved-search page size, clamped to 1-100' },
140-
cursor: { type: 'string', description: 'Opaque saved-search pagination cursor' },
162+
personUrns: {
163+
type: 'array',
164+
description: 'Harmonic person URNs to retrieve or acknowledge',
165+
},
166+
personLinkedinUrls: {
167+
type: 'array',
168+
description: 'LinkedIn profile URLs to submit for email enrichment',
169+
},
170+
clearScope: {
171+
type: 'string',
172+
description: 'Whether to clear only the listed person URNs or every net-new result',
173+
},
174+
jobId: { type: 'string', description: 'Harmonic email enrichment job ID' },
175+
enrichmentUrns: { type: 'array', description: 'Harmonic enrichment URNs to check' },
176+
size: { type: 'number', description: 'Page size, clamped to 1-100' },
177+
cursor: { type: 'string', description: 'Opaque pagination cursor' },
141178
})
142179
})
143180

@@ -253,6 +290,7 @@ describe('HarmonicBlock', () => {
253290
new Set([
254291
'harmonic_search_people_scout',
255292
'harmonic_get_people_saved_search_results',
293+
'harmonic_get_people_saved_search_net_new_results',
256294
'harmonic_batch_get_people',
257295
])
258296
)

0 commit comments

Comments
 (0)