diff --git a/apps/api/src/services/SegmentService.ts b/apps/api/src/services/SegmentService.ts index 6c1a7224..9d8040f3 100644 --- a/apps/api/src/services/SegmentService.ts +++ b/apps/api/src/services/SegmentService.ts @@ -818,7 +818,10 @@ export class SegmentService { value: unknown, unit?: 'days' | 'hours' | 'minutes', ): Prisma.ContactWhereInput { - const path = jsonPath.split('.'); + // Use the entire jsonPath as a single path element. + // Contact data is a flat JSON object, so field names like "prefix.key" + // must be treated as literal keys, not nested paths. + const path = [jsonPath]; switch (operator) { case 'equals': diff --git a/apps/api/src/services/WorkflowExecutionService.ts b/apps/api/src/services/WorkflowExecutionService.ts index a418c815..8426c85d 100644 --- a/apps/api/src/services/WorkflowExecutionService.ts +++ b/apps/api/src/services/WorkflowExecutionService.ts @@ -1111,7 +1111,15 @@ export class WorkflowExecutionService { normalizedField = field.substring(8); // Remove "contact." prefix, leaving "data.X" } - const parts = normalizedField.split('.'); + // Split only on the first dot to separate the top-level field (e.g., "data") + // from the custom field name. This preserves dots in custom field names + // (e.g., "data.prefix.key" → ["data", "prefix.key"]). + const firstDotIndex = normalizedField.indexOf('.'); + const parts = + firstDotIndex === -1 + ? [normalizedField] + : [normalizedField.substring(0, firstDotIndex), normalizedField.substring(firstDotIndex + 1)]; + let value: unknown = data; for (const part of parts) { diff --git a/packages/shared/src/template.ts b/packages/shared/src/template.ts index b1198b24..9abdce7b 100644 --- a/packages/shared/src/template.ts +++ b/packages/shared/src/template.ts @@ -13,13 +13,24 @@ export function renderTemplate(template: string, variables: Record s.trim()); // Handle nested property access (e.g., data.firstName) + // Uses recursive first-dot splitting so that literal dots in custom field + // names (e.g., "prefix.key") are resolved correctly: direct key lookup is + // tried before descending into nested objects. const getValue = (obj: Record, path: string): unknown => { - return path.split('.').reduce((current: Record | unknown, key) => { - if (current && typeof current === 'object' && !Array.isArray(current)) { - return (current as Record)[key]; - } + if (path in obj) { + return obj[path]; + } + const firstDotIndex = path.indexOf('.'); + if (firstDotIndex === -1) { return undefined; - }, obj); + } + const firstKey = path.substring(0, firstDotIndex); + const rest = path.substring(firstDotIndex + 1); + const next = obj[firstKey]; + if (next && typeof next === 'object' && !Array.isArray(next)) { + return getValue(next as Record, rest); + } + return undefined; }; // Try multiple lookup strategies