Skip to content

Commit 0326219

Browse files
committed
feat(connectors): add verified scoping filters (Grain, Rootly, Ashby, Greenhouse, DocuSign, GitLab) + clarify Monday board scope
1 parent 19ffecb commit 0326219

7 files changed

Lines changed: 187 additions & 5 deletions

File tree

apps/sim/connectors/ashby/ashby.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -450,7 +450,18 @@ export const ashbyConnector: ConnectorConfig = {
450450
type: 'short-input',
451451
required: false,
452452
placeholder: 'e.g. 500 (default: unlimited)',
453-
description: 'Cap the number of candidates synced. Leave empty to sync all candidates.',
453+
description:
454+
'Cap the number of candidates synced. Leave empty to sync ALL candidates in the organization.',
455+
},
456+
{
457+
id: 'createdAfter',
458+
title: 'Created After',
459+
type: 'short-input',
460+
required: false,
461+
mode: 'advanced',
462+
placeholder: 'e.g. 2025-01-01 or 2025-01-01T00:00:00Z',
463+
description:
464+
'Only sync candidates created on or after this date (ISO 8601). Leave blank to sync candidates regardless of creation date.',
454465
},
455466
],
456467

@@ -461,6 +472,12 @@ export const ashbyConnector: ConnectorConfig = {
461472
syncContext?: Record<string, unknown>
462473
): Promise<ExternalDocumentList> => {
463474
const maxCandidates = sourceConfig.maxCandidates ? Number(sourceConfig.maxCandidates) : 0
475+
const createdAfterMs = (() => {
476+
const raw = sourceConfig.createdAfter
477+
if (typeof raw !== 'string' || !raw.trim()) return undefined
478+
const ms = new Date(raw.trim()).getTime()
479+
return Number.isNaN(ms) ? undefined : ms
480+
})()
464481

465482
const prevFetched = (syncContext?.totalCandidatesFetched as number) ?? 0
466483
if (maxCandidates > 0 && prevFetched >= maxCandidates) {
@@ -470,6 +487,7 @@ export const ashbyConnector: ConnectorConfig = {
470487

471488
const body: UnknownRecord = { limit: CANDIDATES_PER_PAGE }
472489
if (cursor) body.cursor = cursor
490+
if (createdAfterMs !== undefined) body.createdAfter = createdAfterMs
473491

474492
logger.info('Listing Ashby candidates', {
475493
cursor: cursor ?? 'initial',

apps/sim/connectors/docusign/docusign.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,16 @@ export const docusignConnector: ConnectorConfig = {
436436
description:
437437
'On initial sync only. Filters envelopes by when their status last changed (from_date).',
438438
},
439+
{
440+
id: 'status',
441+
title: 'Filter by Status',
442+
type: 'short-input',
443+
required: false,
444+
mode: 'advanced',
445+
placeholder: 'e.g. completed (or completed,sent)',
446+
description:
447+
'Only sync envelopes with these statuses (comma-separated: created, sent, delivered, completed, declined, voided). Leave blank to sync all.',
448+
},
439449
{
440450
id: 'maxEnvelopes',
441451
title: 'Max Envelopes',
@@ -474,6 +484,8 @@ export const docusignConnector: ConnectorConfig = {
474484
count: String(MAX_PAGE_SIZE),
475485
start_position: String(startPosition),
476486
})
487+
const statusFilter = typeof sourceConfig.status === 'string' ? sourceConfig.status.trim() : ''
488+
if (statusFilter) queryParams.set('status', statusFilter)
477489

478490
const url = `${apiBase}/envelopes?${queryParams.toString()}`
479491

apps/sim/connectors/gitlab/gitlab.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,29 @@ export const gitlabConnector: ConnectorConfig = {
343343
{ label: 'Both', id: 'both' },
344344
],
345345
},
346+
{
347+
id: 'issueState',
348+
title: 'Issue State',
349+
type: 'dropdown',
350+
required: false,
351+
mode: 'advanced',
352+
options: [
353+
{ label: 'All', id: 'all' },
354+
{ label: 'Open only', id: 'opened' },
355+
{ label: 'Closed only', id: 'closed' },
356+
],
357+
description: 'Which issues to sync by state. Applies only when syncing issues.',
358+
},
359+
{
360+
id: 'issueLabels',
361+
title: 'Issue Labels',
362+
type: 'short-input',
363+
required: false,
364+
mode: 'advanced',
365+
placeholder: 'e.g. bug,docs (comma-separated)',
366+
description:
367+
'Only sync issues with all of these labels (comma-separated). Applies only when syncing issues.',
368+
},
346369
{
347370
id: 'maxItems',
348371
title: 'Max Items',
@@ -439,6 +462,12 @@ export const gitlabConnector: ConnectorConfig = {
439462
sort: 'desc',
440463
})
441464
if (lastSyncAt) params.set('updated_after', lastSyncAt.toISOString())
465+
const issueState =
466+
typeof sourceConfig.issueState === 'string' ? sourceConfig.issueState.trim() : ''
467+
if (issueState && issueState !== 'all') params.set('state', issueState)
468+
const issueLabels =
469+
typeof sourceConfig.issueLabels === 'string' ? sourceConfig.issueLabels.trim() : ''
470+
if (issueLabels) params.set('labels', issueLabels)
442471

443472
const url = `${apiBase}/projects/${encodedProject}/issues?${params.toString()}`
444473
logger.info('Listing GitLab issues', {

apps/sim/connectors/grain/grain.ts

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,51 @@ interface GrainTranscriptSegment {
9494
*/
9595
const RECORDING_INCLUDE = { participants: true } as const
9696

97+
/** Number of milliseconds in a day, used to convert the lookback window to a timestamp. */
98+
const MS_PER_DAY = 24 * 60 * 60 * 1000
99+
100+
/**
101+
* Valid values for the recordings list `participant_scope` filter (verified against the
102+
* Grain Public API recordings list request body, which the in-repo Grain tools also use).
103+
*/
104+
const PARTICIPANT_SCOPES = ['internal', 'external'] as const
105+
type ParticipantScope = (typeof PARTICIPANT_SCOPES)[number]
106+
107+
function isParticipantScope(value: unknown): value is ParticipantScope {
108+
return typeof value === 'string' && PARTICIPANT_SCOPES.includes(value as ParticipantScope)
109+
}
110+
111+
/**
112+
* Builds the recordings list `filter` object from the connector's scoping config. Only
113+
* documented Grain filter keys are emitted, and only when configured, so an empty config
114+
* produces no `filter` (full sync). Returns undefined when no scoping is configured.
115+
*
116+
* Supported keys (verified against the in-repo Grain list_recordings tool / Public API):
117+
* - `after_datetime` — derived from `lookbackDays`; recordings on/after the window start
118+
* - `participant_scope` — `internal` or `external`
119+
* - `title_search` — substring match against recording titles
120+
*/
121+
function buildRecordingFilter(
122+
sourceConfig: Record<string, unknown>
123+
): Record<string, unknown> | undefined {
124+
const filter: Record<string, unknown> = {}
125+
126+
const lookbackDays = sourceConfig.lookbackDays ? Number(sourceConfig.lookbackDays) : 0
127+
if (Number.isFinite(lookbackDays) && lookbackDays > 0) {
128+
filter.after_datetime = new Date(Date.now() - lookbackDays * MS_PER_DAY).toISOString()
129+
}
130+
131+
if (isParticipantScope(sourceConfig.participantScope)) {
132+
filter.participant_scope = sourceConfig.participantScope
133+
}
134+
135+
const titleSearch =
136+
typeof sourceConfig.titleSearch === 'string' ? sourceConfig.titleSearch.trim() : ''
137+
if (titleSearch) filter.title_search = titleSearch
138+
139+
return Object.keys(filter).length > 0 ? filter : undefined
140+
}
141+
97142
/**
98143
* Builds the auth + version headers shared by every Grain API request.
99144
*/
@@ -284,6 +329,39 @@ export const grainConnector: ConnectorConfig = {
284329
type: 'short-input',
285330
required: false,
286331
placeholder: 'e.g. 200 (default: unlimited)',
332+
description: 'Cap the total number of recordings synced. Leave blank to sync all.',
333+
},
334+
{
335+
id: 'lookbackDays',
336+
title: 'Lookback Window (days)',
337+
type: 'short-input',
338+
required: false,
339+
mode: 'advanced',
340+
placeholder: 'e.g. 90 (default: all time)',
341+
description: 'Only sync recordings from the last N days. Leave blank to sync any age.',
342+
},
343+
{
344+
id: 'participantScope',
345+
title: 'Participant Scope',
346+
type: 'dropdown',
347+
required: false,
348+
mode: 'advanced',
349+
description:
350+
'Limit to internal-only meetings or meetings that include an external participant. Leave as Any to sync both.',
351+
options: [
352+
{ label: 'Any', id: '' },
353+
{ label: 'Internal only', id: 'internal' },
354+
{ label: 'External (has external participant)', id: 'external' },
355+
],
356+
},
357+
{
358+
id: 'titleSearch',
359+
title: 'Title Search',
360+
type: 'short-input',
361+
required: false,
362+
mode: 'advanced',
363+
placeholder: 'e.g. weekly standup',
364+
description: 'Only sync recordings whose title matches this text. Leave blank to sync all.',
287365
},
288366
],
289367

@@ -298,7 +376,13 @@ export const grainConnector: ConnectorConfig = {
298376
const body: Record<string, unknown> = { include: RECORDING_INCLUDE }
299377
if (cursor) body.cursor = cursor
300378

301-
logger.info('Listing Grain recordings', { hasCursor: Boolean(cursor) })
379+
const filter = buildRecordingFilter(sourceConfig)
380+
if (filter) body.filter = filter
381+
382+
logger.info('Listing Grain recordings', {
383+
hasCursor: Boolean(cursor),
384+
hasFilter: Boolean(filter),
385+
})
302386

303387
const response = await fetchWithRetry(`${GRAIN_API_BASE}/recordings`, {
304388
method: 'POST',

apps/sim/connectors/greenhouse/greenhouse.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -496,7 +496,28 @@ export const greenhouseConnector: ConnectorConfig = {
496496
type: 'short-input',
497497
required: false,
498498
placeholder: 'e.g. 500 (default: unlimited)',
499-
description: 'Cap the number of candidates synced. Leave empty to sync all candidates.',
499+
description:
500+
'Cap the number of candidates synced. Leave empty to sync ALL candidates in the organization.',
501+
},
502+
{
503+
id: 'jobId',
504+
title: 'Job ID',
505+
type: 'short-input',
506+
required: false,
507+
mode: 'advanced',
508+
placeholder: 'e.g. 123456',
509+
description:
510+
'Sync only candidates who applied to this Greenhouse job. Leave empty to sync candidates across all jobs.',
511+
},
512+
{
513+
id: 'createdAfter',
514+
title: 'Created After',
515+
type: 'short-input',
516+
required: false,
517+
mode: 'advanced',
518+
placeholder: 'e.g. 2024-01-01T00:00:00Z',
519+
description:
520+
'Sync only candidates created at or after this ISO 8601 timestamp. Leave empty to sync candidates regardless of creation date.',
500521
},
501522
],
502523

@@ -510,12 +531,17 @@ export const greenhouseConnector: ConnectorConfig = {
510531
const maxCandidates = sourceConfig.maxCandidates ? Number(sourceConfig.maxCandidates) : 0
511532
const page = cursor ? Number(cursor) : 1
512533
const updatedAfter = computeUpdatedAfter(lastSyncAt)
534+
const jobId = typeof sourceConfig.jobId === 'string' ? sourceConfig.jobId.trim() : ''
535+
const createdAfter =
536+
typeof sourceConfig.createdAfter === 'string' ? sourceConfig.createdAfter.trim() : ''
513537

514538
const queryParams = new URLSearchParams({
515539
per_page: String(CANDIDATES_PER_PAGE),
516540
page: String(page),
517541
})
518542
if (updatedAfter) queryParams.set('updated_after', updatedAfter)
543+
if (jobId) queryParams.set('job_id', jobId)
544+
if (createdAfter) queryParams.set('created_after', createdAfter)
519545

520546
const url = `${GREENHOUSE_API_BASE}/candidates?${queryParams.toString()}`
521547

apps/sim/connectors/monday/monday.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -346,8 +346,9 @@ export const mondayConnector: ConnectorConfig = {
346346
title: 'Board IDs',
347347
type: 'short-input',
348348
required: false,
349-
placeholder: 'e.g. 1234567890, 9876543210 (empty = all accessible boards)',
350-
description: 'Comma-separated board IDs to sync. Leave empty to sync all accessible boards.',
349+
placeholder: 'e.g. 1234567890, 9876543210 (empty = all active boards)',
350+
description:
351+
'Comma-separated board IDs to sync — find a board ID in its URL (.../boards/<id>). Leave empty to sync items from every active board you can access.',
351352
},
352353
{
353354
id: 'maxItems',

apps/sim/connectors/rootly/rootly.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,16 @@ export const rootlyConnector: ConnectorConfig = {
382382
placeholder: 'e.g. resolved (default: all)',
383383
description: 'Only sync incidents with this status (e.g. resolved, mitigated, started).',
384384
},
385+
{
386+
id: 'severity',
387+
title: 'Filter by Severity',
388+
type: 'short-input',
389+
required: false,
390+
mode: 'advanced',
391+
placeholder: 'e.g. SEV0 (default: all)',
392+
description:
393+
'Only sync incidents with this severity name. Leave blank to sync all severities.',
394+
},
385395
{
386396
id: 'maxIncidents',
387397
title: 'Max Incidents',
@@ -400,6 +410,7 @@ export const rootlyConnector: ConnectorConfig = {
400410
): Promise<ExternalDocumentList> => {
401411
const maxIncidents = parseMaxIncidents(sourceConfig)
402412
const status = typeof sourceConfig.status === 'string' ? sourceConfig.status.trim() : ''
413+
const severity = typeof sourceConfig.severity === 'string' ? sourceConfig.severity.trim() : ''
403414
const pageNumber = cursor ? Number(cursor) : 1
404415
const startPage = Number.isFinite(pageNumber) && pageNumber > 0 ? pageNumber : 1
405416

@@ -408,6 +419,7 @@ export const rootlyConnector: ConnectorConfig = {
408419
queryParams.set('page[size]', String(PAGE_SIZE))
409420
queryParams.set('include', INCIDENT_INCLUDE)
410421
if (status) queryParams.set('filter[status]', status)
422+
if (severity) queryParams.set('filter[severity]', severity)
411423

412424
// Incremental sync: Rootly supports filtering by update time and sorting by it,
413425
// so the engine only re-reads incidents changed since the last successful sync.

0 commit comments

Comments
 (0)