search-by-timeframe
Search Attio CRM records by creation, modification, or last interaction dates to filter companies, people, tasks, and other resources within specific time periods.
Instructions
Search records by temporal criteria (creation, modification, interaction dates)
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| end_date | No | End date (ISO 8601 format) | |
| limit | No | Maximum number of results to return | |
| offset | No | Number of results to skip for pagination | |
| resource_type | Yes | Type of resource to operate on (companies, people, lists, records, tasks) | |
| start_date | No | Start date (ISO 8601 format) | |
| timeframe_type | No | Timeframe filter type |
Implementation Reference
- Primary handler implementation for the records_search_by_timeframe tool (aliased as search-by-timeframe). Validates params, processes relative dates, builds Attio API date filters based on timeframe_type or date_field, executes search via handleUniversalSearch, and formats results with timestamps.export const searchByTimeframeConfig: UniversalToolConfig< TimeframeSearchParams, AttioRecord[] > = { name: 'records_search_by_timeframe', handler: async (params: TimeframeSearchParams): Promise<AttioRecord[]> => { try { const sanitizedParams = validateUniversalToolParams( 'records_search_by_timeframe', params ); const { resource_type, timeframe_type, start_date, end_date, relative_range, invert_range, date_field, limit, offset, } = sanitizedParams; // Process relative_range parameter if provided (Issue #475) let processedStartDate = start_date; let processedEndDate = end_date; if (relative_range) { // Import the timeframe utility to convert relative ranges const { getRelativeTimeframeRange } = await import( '@utils/filters/timeframe-utils.js' ); try { const range = getRelativeTimeframeRange( relative_range as RelativeTimeframe ); processedStartDate = range.startDate; processedEndDate = range.endDate; } catch { throw new Error( `Invalid relative_range '${relative_range}'. Supported options: today, yesterday, this_week, last_week, this_month, last_month, last_7_days, last_14_days, last_30_days, last_90_days` ); } } // Validate that at least one date is provided (after processing relative_range) if (!processedStartDate && !processedEndDate) { throw new Error( 'At least one date (start_date or end_date) is required for timeframe search' ); } // Determine the timestamp field to filter on (Issue #475) // Use date_field if provided, otherwise fall back to timeframe_type logic let timestampField: string; if (date_field) { // Map date_field directly to proper field name switch (date_field) { case 'created_at': timestampField = mapFieldName('created_at'); break; case 'updated_at': timestampField = mapFieldName('modified_at'); // Map updated_at to modified_at break; case 'modified_at': timestampField = mapFieldName('modified_at'); break; default: throw new Error(`Unsupported date_field: ${date_field}`); } } else { // Fallback to original timeframe_type logic const effectiveTimeframeType = timeframe_type || TimeframeType.MODIFIED; switch (effectiveTimeframeType) { case TimeframeType.CREATED: timestampField = mapFieldName('created_at'); break; case TimeframeType.MODIFIED: timestampField = mapFieldName('modified_at'); break; case TimeframeType.LAST_INTERACTION: timestampField = mapFieldName('modified_at'); break; default: throw new Error( `Unsupported timeframe type: ${effectiveTimeframeType}` ); } } // Build the date filter using proper Attio API v2 filter syntax // Use normalized operators with $ prefix const dateFilters: Record<string, unknown>[] = []; const coerceIso = ( d?: string, endBoundary = false ): string | undefined => { if (!d) return undefined; // If date-only (YYYY-MM-DD), expand to full UTC boundary if (/^\d{4}-\d{2}-\d{2}$/.test(d)) { return endBoundary ? `${d}T23:59:59.999Z` : `${d}T00:00:00Z`; } return d; }; const startIso = coerceIso(processedStartDate, false); const endIso = coerceIso(processedEndDate, true); // Handle invert_range logic (Issue #475) if (invert_range) { // For inverted searches, we want records that were NOT updated in the timeframe // This means records older than the start date OR newer than the end date if (startIso && endIso) { // For a range inversion, we want records outside the range // This is typically records older than the start date (before the timeframe) dateFilters.push({ attribute: { slug: timestampField }, condition: normalizeOperator('lt'), // Less than start date value: startIso, }); } else if (startIso) { // Only start date - invert to find records older than this date dateFilters.push({ attribute: { slug: timestampField }, condition: normalizeOperator('lt'), value: startIso, }); } else if (endIso) { // Only end date - invert to find records newer than this date dateFilters.push({ attribute: { slug: timestampField }, condition: normalizeOperator('gt'), value: endIso, }); } } else { // Normal (non-inverted) logic if (startIso) { dateFilters.push({ attribute: { slug: timestampField }, condition: normalizeOperator('gte'), // Normalize to $gte value: startIso, }); } if (endIso) { dateFilters.push({ attribute: { slug: timestampField }, condition: normalizeOperator('lte'), // Normalize to $lte value: endIso, }); } } // Create the filter object with the expected structure (legacy compatibility) const filters = { filters: dateFilters } as Record<string, unknown>; // Use the universal search handler; pass timeframe params explicitly so the // UniversalSearchService can FORCE Query API routing for date comparisons return await handleUniversalSearch({ resource_type, query: '', filters, // Force timeframe routing parameters timeframe_attribute: timestampField, start_date: startIso, end_date: endIso, date_operator: 'between', limit: limit || 20, offset: offset || 0, }); } catch (error: unknown) { throw ErrorService.createUniversalError( 'records_search_by_timeframe', `${params.resource_type}:${params.timeframe_type || 'undefined'}`, error ); } }, formatResult: (results: AttioRecord[], ...args: unknown[]) => { const timeframeType = args[0] as TimeframeType | undefined; const resourceType = args[1] as UniversalResourceType | undefined; if (!Array.isArray(results)) { return 'Found 0 records (timeframe search)\nTip: Ensure your workspace has data in the requested date range.'; } const timeframeName = timeframeType ? timeframeType.replace(/_/g, ' ') : 'timeframe'; const resourceCount = results.length; const resourceTypeName = resourceType ? resourceCount === 1 ? formatResourceType(resourceType) : getPluralResourceType(resourceType) : resourceCount === 1 ? 'record' : 'records'; return `Found ${results.length} ${resourceTypeName} by ${timeframeName}:\n${results .map((record: Record<string, unknown>, index: number) => { const values = record.values as Record<string, unknown>; const name = (values?.name as Record<string, unknown>[])?.[0]?.value || (values?.name as Record<string, unknown>[])?.[0]?.full_name || (values?.full_name as Record<string, unknown>[])?.[0]?.value || (values?.title as Record<string, unknown>[])?.[0]?.value || 'Unnamed'; const recordId = record.id as Record<string, unknown>; const id = recordId?.record_id || 'unknown'; // Try to show relevant date information const created = safeExtractTimestamp(record.created_at); const modified = safeExtractTimestamp(record.updated_at); let dateInfo = ''; if (timeframeType === TimeframeType.CREATED && created !== 'unknown') { dateInfo = ` (created: ${new Date(created).toLocaleDateString()})`; } else if ( timeframeType === TimeframeType.MODIFIED && modified !== 'unknown' ) { dateInfo = ` (modified: ${new Date(modified).toLocaleDateString()})`; } return `${index + 1}. ${name}${dateInfo} (ID: ${id})`; }) .join('\n')}`; }, };
- Zod/JSON schema for input validation of the search-by-timeframe tool parameters, including resource_type, timeframe_type, start_date, end_date, and pagination.export const searchByTimeframeSchema = { type: 'object' as const, properties: { resource_type: resourceTypeProperty, timeframe_type: { type: 'string' as const, enum: Object.values(TimeframeType), description: 'Timeframe filter type', }, start_date: { type: 'string' as const, format: 'date', description: 'Start date (ISO 8601 format)', }, end_date: { type: 'string' as const, format: 'date', description: 'End date (ISO 8601 format)', }, ...paginationProperties, }, required: ['resource_type' as const], additionalProperties: false, examples: [ { resource_type: 'deals', timeframe_type: TimeframeType.CREATED, start_date: '2025-01-01', end_date: '2025-01-31', }, ], };
- MCP tool definition registration for records_search_by_timeframe, including description, inputSchema reference, and annotations. Exported via advancedOperationsToolDefinitions and included in universalToolDefinitions.records_search_by_timeframe: { name: 'records_search_by_timeframe', description: formatToolDescription({ capability: 'Filter records by creation, update, or interaction timeframes.', boundaries: 'modify lifecycle state or scheduling follow-ups.', constraints: 'Requires resource_type; provide timeframe or explicit date boundaries.', recoveryHint: 'Call records.search if timeframe filters are too restrictive.', }), inputSchema: searchByTimeframeSchema, annotations: { readOnlyHint: true, idempotentHint: true, }, },
- src/config/tool-aliases.ts:67-72 (registration)Tool alias registration mapping 'search-by-timeframe' to the canonical 'records_search_by_timeframe' tool.'search-by-timeframe': { target: 'records_search_by_timeframe', reason: 'Phase 1 search tool rename (#776)', since: SINCE_PHASE_1, removal: 'v1.x (TBD)', },
- Helper mapping for deprecated tool names to timeframe_type values used in migration params for search-by-timeframe.* Timeframe type mappings for search-by-timeframe universal tool */ export const timeframeTypeMappings: Record<string, string> = { 'search-people-by-creation-date': 'created', 'search-people-by-modification-date': 'modified', 'search-people-by-last-interaction': 'last_interaction', };