From 788320b68b3daade00c3bb3d54759722ae7da207 Mon Sep 17 00:00:00 2001 From: waleed Date: Wed, 1 Jul 2026 23:48:19 -0700 Subject: [PATCH 1/4] fix(loops): align integration with live API docs, add suppression + get-template tools - fix list_transactional_emails endpoint URL (was /transactional, now /transactional-emails) - fix response fields to match actual API schema (createdAt/updatedAt, not the never-existent lastUpdated) - add loops_check_contact_suppression, loops_remove_contact_suppression, loops_get_transactional_email tools - wire new tools into block operations, outputs, and registries - alphabetize tools/registry.ts loops entries --- apps/sim/blocks/blocks/loops.ts | 93 ++++++++++++++- .../tools/loops/check_contact_suppression.ts | 104 +++++++++++++++++ .../tools/loops/get_transactional_email.ts | 106 ++++++++++++++++++ apps/sim/tools/loops/index.ts | 3 + .../tools/loops/list_transactional_emails.ts | 8 +- .../tools/loops/remove_contact_suppression.ts | 99 ++++++++++++++++ apps/sim/tools/loops/types.ts | 53 ++++++++- apps/sim/tools/registry.ts | 12 +- 8 files changed, 465 insertions(+), 13 deletions(-) create mode 100644 apps/sim/tools/loops/check_contact_suppression.ts create mode 100644 apps/sim/tools/loops/get_transactional_email.ts create mode 100644 apps/sim/tools/loops/remove_contact_suppression.ts diff --git a/apps/sim/blocks/blocks/loops.ts b/apps/sim/blocks/blocks/loops.ts index 76e27e48cb0..638986b2f38 100644 --- a/apps/sim/blocks/blocks/loops.ts +++ b/apps/sim/blocks/blocks/loops.ts @@ -32,6 +32,9 @@ export const LoopsBlock: BlockConfig = { { label: 'List Transactional Emails', id: 'list_transactional_emails' }, { label: 'Create Contact Property', id: 'create_contact_property' }, { label: 'List Contact Properties', id: 'list_contact_properties' }, + { label: 'Check Contact Suppression', id: 'check_contact_suppression' }, + { label: 'Remove Contact Suppression', id: 'remove_contact_suppression' }, + { label: 'Get Transactional Email', id: 'get_transactional_email' }, ], value: () => 'create_contact', }, @@ -47,7 +50,7 @@ export const LoopsBlock: BlockConfig = { value: ['create_contact', 'send_transactional_email'], }, }, - // Optional email for update, find, delete, send event + // Optional email for update, find, delete, send event, suppression lookups { id: 'contactEmail', title: 'Email', @@ -55,7 +58,14 @@ export const LoopsBlock: BlockConfig = { placeholder: 'Enter email address', condition: { field: 'operation', - value: ['update_contact', 'find_contact', 'delete_contact', 'send_event'], + value: [ + 'update_contact', + 'find_contact', + 'delete_contact', + 'send_event', + 'check_contact_suppression', + 'remove_contact_suppression', + ], }, }, // User ID for operations that support it @@ -66,7 +76,14 @@ export const LoopsBlock: BlockConfig = { placeholder: 'Enter user ID', condition: { field: 'operation', - value: ['update_contact', 'find_contact', 'delete_contact', 'send_event'], + value: [ + 'update_contact', + 'find_contact', + 'delete_contact', + 'send_event', + 'check_contact_suppression', + 'remove_contact_suppression', + ], }, }, // Contact fields @@ -199,10 +216,13 @@ Return ONLY the JSON object - no explanations, no extra text.`, title: 'Transactional Email ID', type: 'short-input', placeholder: 'Enter template ID (e.g., clx...)', - required: { field: 'operation', value: 'send_transactional_email' }, + required: { + field: 'operation', + value: ['send_transactional_email', 'get_transactional_email'], + }, condition: { field: 'operation', - value: 'send_transactional_email', + value: ['send_transactional_email', 'get_transactional_email'], }, }, { @@ -426,6 +446,9 @@ Return ONLY the JSON object - no explanations, no extra text.`, 'loops_list_transactional_emails', 'loops_create_contact_property', 'loops_list_contact_properties', + 'loops_check_contact_suppression', + 'loops_remove_contact_suppression', + 'loops_get_transactional_email', ], config: { tool: (params) => `loops_${params.operation}`, @@ -497,6 +520,16 @@ Return ONLY the JSON object - no explanations, no extra text.`, case 'list_contact_properties': if (params.propertyFilter) result.list = params.propertyFilter break + + case 'check_contact_suppression': + case 'remove_contact_suppression': + if (params.contactEmail) result.email = params.contactEmail + if (params.userId) result.userId = params.userId + break + + case 'get_transactional_email': + result.transactionalId = params.transactionalId + break } return result @@ -544,7 +577,8 @@ Return ONLY the JSON object - no explanations, no extra text.`, }, transactionalEmails: { type: 'json', - description: 'Array of transactional email templates (id, name, lastUpdated, dataVariables)', + description: + 'Array of transactional email templates (id, name, createdAt, updatedAt, dataVariables)', }, pagination: { type: 'json', @@ -555,6 +589,46 @@ Return ONLY the JSON object - no explanations, no extra text.`, type: 'json', description: 'Array of contact properties (key, label, type)', }, + isSuppressed: { + type: 'boolean', + description: 'Whether the contact is on the suppression list (check suppression)', + }, + removalQuotaLimit: { + type: 'number', + description: 'Total suppression-removal quota for the team', + }, + removalQuotaRemaining: { + type: 'number', + description: 'Remaining suppression-removal quota for the team', + }, + name: { + type: 'string', + description: 'Transactional email template name (get transactional email)', + }, + draftEmailMessageId: { + type: 'string', + description: 'ID of the draft email message, if any (get transactional email)', + }, + publishedEmailMessageId: { + type: 'string', + description: 'ID of the published email message, if any (get transactional email)', + }, + transactionalGroupId: { + type: 'string', + description: 'ID of the transactional group, if any (get transactional email)', + }, + createdAt: { + type: 'string', + description: 'Creation timestamp (get transactional email)', + }, + updatedAt: { + type: 'string', + description: 'Last updated timestamp (get transactional email)', + }, + dataVariables: { + type: 'json', + description: 'Template data variable names (get transactional email)', + }, }, } @@ -655,5 +729,12 @@ export const LoopsBlockMeta = { content: '# Send Transactional Email\n\nDeliver a templated transactional email through Loops.\n\n## Steps\n1. Confirm the transactional email template ID to use.\n2. Build the data variables JSON to match the variable names in the template, such as name and a confirmation URL.\n3. Send Transactional Email with the recipient email, template ID, and data variables, attaching files if needed.\n\n## Output\nConfirmation of send success and the template ID and recipient used.', }, + { + name: 'manage-suppression-compliance', + description: + 'Check and clear Loops suppression status for a contact to keep deliverability and unsubscribe compliance in check.', + content: + '# Manage Suppression Compliance\n\nKeep Loops sending compliant and deliverable.\n\n## Steps\n1. Check Contact Suppression by email or user ID to see if the contact bounced, complained, or unsubscribed.\n2. If the contact should be re-enabled (e.g. a confirmed re-opt-in), Remove Contact Suppression for the same identifier, noting the remaining removal quota.\n3. Log the result so support and compliance workflows have an audit trail.\n\n## Output\nThe suppression status before and after the change, plus the remaining removal quota.', + }, ], } as const satisfies BlockMeta diff --git a/apps/sim/tools/loops/check_contact_suppression.ts b/apps/sim/tools/loops/check_contact_suppression.ts new file mode 100644 index 00000000000..bcc8aa922e0 --- /dev/null +++ b/apps/sim/tools/loops/check_contact_suppression.ts @@ -0,0 +1,104 @@ +import type { + LoopsCheckContactSuppressionParams, + LoopsCheckContactSuppressionResponse, +} from '@/tools/loops/types' +import type { ToolConfig } from '@/tools/types' + +export const loopsCheckContactSuppressionTool: ToolConfig< + LoopsCheckContactSuppressionParams, + LoopsCheckContactSuppressionResponse +> = { + id: 'loops_check_contact_suppression', + name: 'Loops Check Contact Suppression', + description: + 'Check whether a Loops contact is on the suppression list (bounced, complained, or unsubscribed) by email address or userId.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Loops API key for authentication', + }, + email: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'The contact email address to check (at least one of email or userId is required)', + }, + userId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'The contact userId to check (at least one of email or userId is required)', + }, + }, + + request: { + url: (params) => { + if (!params.email && !params.userId) { + throw new Error('At least one of email or userId is required to check suppression status') + } + const base = 'https://app.loops.so/api/v1/contacts/suppression' + if (params.email) return `${base}?email=${encodeURIComponent(params.email.trim())}` + return `${base}?userId=${encodeURIComponent(params.userId!.trim())}` + }, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.apiKey}`, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (data.isSuppressed == null) { + return { + success: false, + output: { + contactId: null, + email: null, + userId: null, + isSuppressed: false, + removalQuotaLimit: null, + removalQuotaRemaining: null, + }, + error: data.message ?? 'Failed to check contact suppression status', + } + } + + return { + success: true, + output: { + contactId: (data.contact?.id as string) ?? null, + email: (data.contact?.email as string) ?? null, + userId: (data.contact?.userId as string) ?? null, + isSuppressed: (data.isSuppressed as boolean) ?? false, + removalQuotaLimit: (data.removalQuota?.limit as number) ?? null, + removalQuotaRemaining: (data.removalQuota?.remaining as number) ?? null, + }, + } + }, + + outputs: { + contactId: { type: 'string', description: 'The Loops-assigned contact ID', optional: true }, + email: { type: 'string', description: 'The contact email address', optional: true }, + userId: { type: 'string', description: 'The contact userId', optional: true }, + isSuppressed: { + type: 'boolean', + description: 'Whether the contact is on the suppression list', + }, + removalQuotaLimit: { + type: 'number', + description: 'Total suppression-removal quota for the team', + optional: true, + }, + removalQuotaRemaining: { + type: 'number', + description: 'Remaining suppression-removal quota for the team', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/loops/get_transactional_email.ts b/apps/sim/tools/loops/get_transactional_email.ts new file mode 100644 index 00000000000..55fd75d6f16 --- /dev/null +++ b/apps/sim/tools/loops/get_transactional_email.ts @@ -0,0 +1,106 @@ +import type { + LoopsGetTransactionalEmailParams, + LoopsGetTransactionalEmailResponse, +} from '@/tools/loops/types' +import type { ToolConfig } from '@/tools/types' + +export const loopsGetTransactionalEmailTool: ToolConfig< + LoopsGetTransactionalEmailParams, + LoopsGetTransactionalEmailResponse +> = { + id: 'loops_get_transactional_email', + name: 'Loops Get Transactional Email', + description: + 'Retrieve a single transactional email template from your Loops account by its ID, including its data variables and draft/published message IDs.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Loops API key for authentication', + }, + transactionalId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the transactional email template to retrieve', + }, + }, + + request: { + url: (params) => + `https://app.loops.so/api/v1/transactional-emails/${encodeURIComponent(params.transactionalId.trim())}`, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.apiKey}`, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!data.id) { + return { + success: false, + output: { + id: null, + name: null, + draftEmailMessageId: null, + publishedEmailMessageId: null, + transactionalGroupId: null, + createdAt: null, + updatedAt: null, + dataVariables: [], + }, + error: data.message ?? 'Failed to get transactional email', + } + } + + return { + success: true, + output: { + id: (data.id as string) ?? null, + name: (data.name as string) ?? null, + draftEmailMessageId: (data.draftEmailMessageId as string) ?? null, + publishedEmailMessageId: (data.publishedEmailMessageId as string) ?? null, + transactionalGroupId: (data.transactionalGroupId as string) ?? null, + createdAt: (data.createdAt as string) ?? null, + updatedAt: (data.updatedAt as string) ?? null, + dataVariables: (data.dataVariables as string[]) ?? [], + }, + } + }, + + outputs: { + id: { type: 'string', description: 'The transactional email template ID', optional: true }, + name: { type: 'string', description: 'The template name', optional: true }, + draftEmailMessageId: { + type: 'string', + description: 'ID of the draft email message, if any', + optional: true, + }, + publishedEmailMessageId: { + type: 'string', + description: 'ID of the published email message, if any', + optional: true, + }, + transactionalGroupId: { + type: 'string', + description: 'ID of the transactional group this template belongs to, if any', + optional: true, + }, + createdAt: { type: 'string', description: 'Creation timestamp (ISO 8601)', optional: true }, + updatedAt: { + type: 'string', + description: 'Last updated timestamp (ISO 8601)', + optional: true, + }, + dataVariables: { + type: 'array', + description: 'Template data variable names', + items: { type: 'string' }, + }, + }, +} diff --git a/apps/sim/tools/loops/index.ts b/apps/sim/tools/loops/index.ts index bad17376cf5..baa5480634c 100644 --- a/apps/sim/tools/loops/index.ts +++ b/apps/sim/tools/loops/index.ts @@ -1,10 +1,13 @@ +export { loopsCheckContactSuppressionTool } from '@/tools/loops/check_contact_suppression' export { loopsCreateContactTool } from '@/tools/loops/create_contact' export { loopsCreateContactPropertyTool } from '@/tools/loops/create_contact_property' export { loopsDeleteContactTool } from '@/tools/loops/delete_contact' export { loopsFindContactTool } from '@/tools/loops/find_contact' +export { loopsGetTransactionalEmailTool } from '@/tools/loops/get_transactional_email' export { loopsListContactPropertiesTool } from '@/tools/loops/list_contact_properties' export { loopsListMailingListsTool } from '@/tools/loops/list_mailing_lists' export { loopsListTransactionalEmailsTool } from '@/tools/loops/list_transactional_emails' +export { loopsRemoveContactSuppressionTool } from '@/tools/loops/remove_contact_suppression' export { loopsSendEventTool } from '@/tools/loops/send_event' export { loopsSendTransactionalEmailTool } from '@/tools/loops/send_transactional_email' export { loopsUpdateContactTool } from '@/tools/loops/update_contact' diff --git a/apps/sim/tools/loops/list_transactional_emails.ts b/apps/sim/tools/loops/list_transactional_emails.ts index bd36d373222..8874a08e41c 100644 --- a/apps/sim/tools/loops/list_transactional_emails.ts +++ b/apps/sim/tools/loops/list_transactional_emails.ts @@ -37,7 +37,7 @@ export const loopsListTransactionalEmailsTool: ToolConfig< request: { url: (params) => { - const base = 'https://app.loops.so/api/v1/transactional' + const base = 'https://app.loops.so/api/v1/transactional-emails' const queryParams: string[] = [] if (params.perPage) queryParams.push(`perPage=${encodeURIComponent(params.perPage)}`) if (params.cursor) queryParams.push(`cursor=${encodeURIComponent(params.cursor)}`) @@ -78,7 +78,8 @@ export const loopsListTransactionalEmailsTool: ToolConfig< transactionalEmails: emails.map((email: Record) => ({ id: (email.id as string) ?? '', name: (email.name as string) ?? '', - lastUpdated: (email.lastUpdated as string) ?? '', + createdAt: (email.createdAt as string) ?? '', + updatedAt: (email.updatedAt as string) ?? '', dataVariables: (email.dataVariables as string[]) ?? [], })), pagination: { @@ -102,7 +103,8 @@ export const loopsListTransactionalEmailsTool: ToolConfig< properties: { id: { type: 'string', description: 'The transactional email template ID' }, name: { type: 'string', description: 'The template name' }, - lastUpdated: { type: 'string', description: 'Last updated timestamp' }, + createdAt: { type: 'string', description: 'Creation timestamp (ISO 8601)' }, + updatedAt: { type: 'string', description: 'Last updated timestamp (ISO 8601)' }, dataVariables: { type: 'array', description: 'Template data variable names', diff --git a/apps/sim/tools/loops/remove_contact_suppression.ts b/apps/sim/tools/loops/remove_contact_suppression.ts new file mode 100644 index 00000000000..0198d3b13f7 --- /dev/null +++ b/apps/sim/tools/loops/remove_contact_suppression.ts @@ -0,0 +1,99 @@ +import type { + LoopsRemoveContactSuppressionParams, + LoopsRemoveContactSuppressionResponse, +} from '@/tools/loops/types' +import type { ToolConfig } from '@/tools/types' + +export const loopsRemoveContactSuppressionTool: ToolConfig< + LoopsRemoveContactSuppressionParams, + LoopsRemoveContactSuppressionResponse +> = { + id: 'loops_remove_contact_suppression', + name: 'Loops Remove Contact Suppression', + description: + 'Remove a Loops contact from the suppression list by email address or userId, allowing them to receive emails again. Subject to a team removal quota.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Loops API key for authentication', + }, + email: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'The contact email address to remove from suppression (at least one of email or userId is required)', + }, + userId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'The contact userId to remove from suppression (at least one of email or userId is required)', + }, + }, + + request: { + url: (params) => { + if (!params.email && !params.userId) { + throw new Error('At least one of email or userId is required to remove suppression') + } + const base = 'https://app.loops.so/api/v1/contacts/suppression' + if (params.email) return `${base}?email=${encodeURIComponent(params.email.trim())}` + return `${base}?userId=${encodeURIComponent(params.userId!.trim())}` + }, + method: 'DELETE', + headers: (params) => ({ + Authorization: `Bearer ${params.apiKey}`, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!data.success) { + return { + success: false, + output: { + success: false, + message: data.message ?? 'Failed to remove contact suppression', + removalQuotaLimit: (data.removalQuota?.limit as number) ?? null, + removalQuotaRemaining: (data.removalQuota?.remaining as number) ?? null, + }, + error: data.message ?? 'Failed to remove contact suppression', + } + } + + return { + success: true, + output: { + success: true, + message: data.message ?? 'Contact removed from suppression list.', + removalQuotaLimit: (data.removalQuota?.limit as number) ?? null, + removalQuotaRemaining: (data.removalQuota?.remaining as number) ?? null, + }, + } + }, + + outputs: { + success: { + type: 'boolean', + description: 'Whether the contact was removed from suppression successfully', + }, + message: { type: 'string', description: 'Status message from the API', optional: true }, + removalQuotaLimit: { + type: 'number', + description: 'Total suppression-removal quota for the team', + optional: true, + }, + removalQuotaRemaining: { + type: 'number', + description: 'Remaining suppression-removal quota for the team', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/loops/types.ts b/apps/sim/tools/loops/types.ts index da3f81d4c91..6a62444cf5e 100644 --- a/apps/sim/tools/loops/types.ts +++ b/apps/sim/tools/loops/types.ts @@ -70,6 +70,20 @@ export interface LoopsListContactPropertiesParams extends LoopsBaseParams { list?: string } +export interface LoopsCheckContactSuppressionParams extends LoopsBaseParams { + email?: string + userId?: string +} + +export interface LoopsRemoveContactSuppressionParams extends LoopsBaseParams { + email?: string + userId?: string +} + +export interface LoopsGetTransactionalEmailParams extends LoopsBaseParams { + transactionalId: string +} + interface LoopsContact { id: string email: string @@ -138,7 +152,8 @@ export interface LoopsListTransactionalEmailsResponse extends ToolResponse { transactionalEmails: { id: string name: string - lastUpdated: string + createdAt: string + updatedAt: string dataVariables: string[] }[] pagination: { @@ -168,6 +183,39 @@ export interface LoopsListContactPropertiesResponse extends ToolResponse { } } +export interface LoopsCheckContactSuppressionResponse extends ToolResponse { + output: { + contactId: string | null + email: string | null + userId: string | null + isSuppressed: boolean + removalQuotaLimit: number | null + removalQuotaRemaining: number | null + } +} + +export interface LoopsRemoveContactSuppressionResponse extends ToolResponse { + output: { + success: boolean + message: string | null + removalQuotaLimit: number | null + removalQuotaRemaining: number | null + } +} + +export interface LoopsGetTransactionalEmailResponse extends ToolResponse { + output: { + id: string | null + name: string | null + draftEmailMessageId: string | null + publishedEmailMessageId: string | null + transactionalGroupId: string | null + createdAt: string | null + updatedAt: string | null + dataVariables: string[] + } +} + export type LoopsResponse = | LoopsCreateContactResponse | LoopsUpdateContactResponse @@ -179,6 +227,9 @@ export type LoopsResponse = | LoopsListTransactionalEmailsResponse | LoopsCreateContactPropertyResponse | LoopsListContactPropertiesResponse + | LoopsCheckContactSuppressionResponse + | LoopsRemoveContactSuppressionResponse + | LoopsGetTransactionalEmailResponse export const LOOPS_CONTACT_OUTPUT_PROPERTIES = { id: { type: 'string' as const, description: 'Loops-assigned contact ID' }, diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 2bb910c4eef..1e8114b93bc 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -2065,13 +2065,16 @@ import { logsQueryTool, } from '@/tools/logs' import { + loopsCheckContactSuppressionTool, loopsCreateContactPropertyTool, loopsCreateContactTool, loopsDeleteContactTool, loopsFindContactTool, + loopsGetTransactionalEmailTool, loopsListContactPropertiesTool, loopsListMailingListsTool, loopsListTransactionalEmailsTool, + loopsRemoveContactSuppressionTool, loopsSendEventTool, loopsSendTransactionalEmailTool, loopsUpdateContactTool, @@ -4633,16 +4636,19 @@ export const tools: Record = { logs_get: logsGetTool, logs_get_execution: logsGetExecutionTool, logs_get_run_details: logsGetRunDetailsTool, + loops_check_contact_suppression: loopsCheckContactSuppressionTool, loops_create_contact: loopsCreateContactTool, loops_create_contact_property: loopsCreateContactPropertyTool, - loops_update_contact: loopsUpdateContactTool, - loops_find_contact: loopsFindContactTool, loops_delete_contact: loopsDeleteContactTool, + loops_find_contact: loopsFindContactTool, + loops_get_transactional_email: loopsGetTransactionalEmailTool, loops_list_contact_properties: loopsListContactPropertiesTool, loops_list_mailing_lists: loopsListMailingListsTool, loops_list_transactional_emails: loopsListTransactionalEmailsTool, - loops_send_transactional_email: loopsSendTransactionalEmailTool, + loops_remove_contact_suppression: loopsRemoveContactSuppressionTool, loops_send_event: loopsSendEventTool, + loops_send_transactional_email: loopsSendTransactionalEmailTool, + loops_update_contact: loopsUpdateContactTool, luma_add_guests: lumaAddGuestsTool, luma_cancel_event: lumaCancelEventTool, luma_create_event: lumaCreateEventTool, From 7c49c26f9669e398f2a423e1518e7cec9829aefe Mon Sep 17 00:00:00 2001 From: waleed Date: Wed, 1 Jul 2026 23:55:50 -0700 Subject: [PATCH 2/4] fix(loops): expose contactId output, fix stale tool description - add missing contactId block output for check_contact_suppression (Greptile P1) - fix list_transactional_emails description to mention createdAt (Greptile P2) --- apps/sim/blocks/blocks/loops.ts | 4 ++++ apps/sim/tools/loops/list_transactional_emails.ts | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/sim/blocks/blocks/loops.ts b/apps/sim/blocks/blocks/loops.ts index 638986b2f38..cac26594aeb 100644 --- a/apps/sim/blocks/blocks/loops.ts +++ b/apps/sim/blocks/blocks/loops.ts @@ -593,6 +593,10 @@ Return ONLY the JSON object - no explanations, no extra text.`, type: 'boolean', description: 'Whether the contact is on the suppression list (check suppression)', }, + contactId: { + type: 'string', + description: 'The Loops-assigned contact ID (check suppression)', + }, removalQuotaLimit: { type: 'number', description: 'Total suppression-removal quota for the team', diff --git a/apps/sim/tools/loops/list_transactional_emails.ts b/apps/sim/tools/loops/list_transactional_emails.ts index 8874a08e41c..fe9f7962764 100644 --- a/apps/sim/tools/loops/list_transactional_emails.ts +++ b/apps/sim/tools/loops/list_transactional_emails.ts @@ -11,7 +11,7 @@ export const loopsListTransactionalEmailsTool: ToolConfig< id: 'loops_list_transactional_emails', name: 'Loops List Transactional Emails', description: - 'Retrieve a list of published transactional email templates from your Loops account. Returns each template with its ID, name, last updated timestamp, and data variables.', + 'Retrieve a list of published transactional email templates from your Loops account. Returns each template with its ID, name, created/updated timestamps, and data variables.', version: '1.0.0', params: { From 57014a3c84ee988a3d479a79148674115d5d9075 Mon Sep 17 00:00:00 2001 From: waleed Date: Thu, 2 Jul 2026 00:32:09 -0700 Subject: [PATCH 3/4] fix(loops): restore lastUpdated as backwards-compat alias on list_transactional_emails Final validation pass found that /api/v1/transactional (the endpoint this tool used before this PR) is a real, functional, deprecated Loops endpoint whose schema genuinely returns lastUpdated - it was not a broken/invented field. Migrating to /api/v1/transactional-emails is still correct (current endpoint, better error semantics), but dropping lastUpdated would break any existing workflow reading it from this block's output. Keep it as a deprecated alias of updatedAt alongside the new createdAt/updatedAt fields. --- apps/sim/blocks/blocks/loops.ts | 2 +- apps/sim/tools/loops/list_transactional_emails.ts | 7 +++++++ apps/sim/tools/loops/types.ts | 2 ++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/apps/sim/blocks/blocks/loops.ts b/apps/sim/blocks/blocks/loops.ts index cac26594aeb..adb14dd84fe 100644 --- a/apps/sim/blocks/blocks/loops.ts +++ b/apps/sim/blocks/blocks/loops.ts @@ -578,7 +578,7 @@ Return ONLY the JSON object - no explanations, no extra text.`, transactionalEmails: { type: 'json', description: - 'Array of transactional email templates (id, name, createdAt, updatedAt, dataVariables)', + 'Array of transactional email templates (id, name, createdAt, updatedAt, lastUpdated (deprecated alias of updatedAt), dataVariables)', }, pagination: { type: 'json', diff --git a/apps/sim/tools/loops/list_transactional_emails.ts b/apps/sim/tools/loops/list_transactional_emails.ts index fe9f7962764..30703e87a31 100644 --- a/apps/sim/tools/loops/list_transactional_emails.ts +++ b/apps/sim/tools/loops/list_transactional_emails.ts @@ -80,6 +80,9 @@ export const loopsListTransactionalEmailsTool: ToolConfig< name: (email.name as string) ?? '', createdAt: (email.createdAt as string) ?? '', updatedAt: (email.updatedAt as string) ?? '', + // Deprecated alias of updatedAt, kept for backwards compatibility with the old + // (now-removed) /api/v1/transactional list endpoint, which returned this field. + lastUpdated: (email.updatedAt as string) ?? '', dataVariables: (email.dataVariables as string[]) ?? [], })), pagination: { @@ -105,6 +108,10 @@ export const loopsListTransactionalEmailsTool: ToolConfig< name: { type: 'string', description: 'The template name' }, createdAt: { type: 'string', description: 'Creation timestamp (ISO 8601)' }, updatedAt: { type: 'string', description: 'Last updated timestamp (ISO 8601)' }, + lastUpdated: { + type: 'string', + description: 'Deprecated alias of updatedAt, kept for backwards compatibility', + }, dataVariables: { type: 'array', description: 'Template data variable names', diff --git a/apps/sim/tools/loops/types.ts b/apps/sim/tools/loops/types.ts index 6a62444cf5e..79b0e3626b6 100644 --- a/apps/sim/tools/loops/types.ts +++ b/apps/sim/tools/loops/types.ts @@ -154,6 +154,8 @@ export interface LoopsListTransactionalEmailsResponse extends ToolResponse { name: string createdAt: string updatedAt: string + /** @deprecated Alias of updatedAt, kept for backwards compatibility */ + lastUpdated: string dataVariables: string[] }[] pagination: { From bf5c0d5d95238defe3e7e04abbcfdaf345894acc Mon Sep 17 00:00:00 2001 From: waleed Date: Thu, 2 Jul 2026 08:34:23 -0700 Subject: [PATCH 4/4] fix(loops): update id output description to cover get_transactional_email --- apps/sim/blocks/blocks/loops.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/sim/blocks/blocks/loops.ts b/apps/sim/blocks/blocks/loops.ts index adb14dd84fe..c71ce37e1d4 100644 --- a/apps/sim/blocks/blocks/loops.ts +++ b/apps/sim/blocks/blocks/loops.ts @@ -564,7 +564,10 @@ Return ONLY the JSON object - no explanations, no extra text.`, }, outputs: { success: { type: 'boolean', description: 'Whether the operation succeeded' }, - id: { type: 'string', description: 'Contact ID (create/update operations)' }, + id: { + type: 'string', + description: 'Contact ID (create/update operations) or template ID (get transactional email)', + }, contacts: { type: 'json', description: