Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 95 additions & 7 deletions apps/sim/blocks/blocks/loops.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ export const LoopsBlock: BlockConfig<LoopsResponse> = {
{ 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',
},
Expand All @@ -47,15 +50,22 @@ export const LoopsBlock: BlockConfig<LoopsResponse> = {
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',
type: 'short-input',
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
Expand All @@ -66,7 +76,14 @@ export const LoopsBlock: BlockConfig<LoopsResponse> = {
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
Expand Down Expand Up @@ -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'],
},
},
{
Expand Down Expand Up @@ -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}`,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -531,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:
Expand All @@ -544,7 +580,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, lastUpdated (deprecated alias of updatedAt), dataVariables)',
},
pagination: {
type: 'json',
Expand All @@ -555,6 +592,50 @@ 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)',
},
contactId: {
type: 'string',
description: 'The Loops-assigned contact ID (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)',
},
Comment thread
waleedlatif1 marked this conversation as resolved.
},
}

Expand Down Expand Up @@ -655,5 +736,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
104 changes: 104 additions & 0 deletions apps/sim/tools/loops/check_contact_suppression.ts
Original file line number Diff line number Diff line change
@@ -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,
},
},
}
106 changes: 106 additions & 0 deletions apps/sim/tools/loops/get_transactional_email.ts
Original file line number Diff line number Diff line change
@@ -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' },
},
},
}
Loading
Loading