feat(loops): add Loops email platform integration (#3359)

* feat(loops): add Loops email platform integration

Add complete Loops integration with 10 tools covering all API endpoints:
- Contact management: create, update, find, delete
- Email: send transactional emails with attachments
- Events: trigger automated email sequences
- Lists: list mailing lists and transactional email templates
- Properties: create and list contact properties

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* ran litn

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Waleed
2026-02-26 22:09:02 -08:00
committed by GitHub
parent c0f22d7722
commit f193e9ebbc
20 changed files with 2147 additions and 0 deletions

View File

@@ -3980,6 +3980,17 @@ export function IntercomIcon(props: SVGProps<SVGSVGElement>) {
)
}
export function LoopsIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg {...props} viewBox='0 0 256 256' fill='none' xmlns='http://www.w3.org/2000/svg'>
<path
fill='currentColor'
d='M192.352 88.042c0-7.012-5.685-12.697-12.697-12.697s-12.697 5.685-12.697 12.697c0 .634.052 1.255.142 1.866a25.248 25.248 0 0 0-4.9-.49c-14.006 0-25.36 11.354-25.36 25.36 0 1.63.16 3.222.456 4.765a37.8 37.8 0 0 0-9.296-1.173c-20.95 0-37.935 16.985-37.935 37.935S107.05 194.24 128 194.24s37.935-16.985 37.935-37.935a37.7 37.7 0 0 0-3.78-16.555 25.2 25.2 0 0 0 12.487-3.336 25.2 25.2 0 0 0 4.558 3.336v.02c14.006 0 25.36-11.354 25.36-25.36 0-12.48-9.018-22.855-20.888-24.996a12.6 12.6 0 0 0 8.68-11.972m-77.05 68.263c0-7.012 5.685-12.697 12.697-12.697s12.697 5.685 12.697 12.697c0 7.013-5.685 12.697-12.697 12.697s-12.697-5.685-12.697-12.697'
/>
</svg>
)
}
export function LumaIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg {...props} fill='none' viewBox='0 0 133 134' xmlns='http://www.w3.org/2000/svg'>

View File

@@ -80,6 +80,7 @@ import {
LinearIcon,
LinkedInIcon,
LinkupIcon,
LoopsIcon,
LumaIcon,
MailchimpIcon,
MailgunIcon,
@@ -236,6 +237,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
linear: LinearIcon,
linkedin: LinkedInIcon,
linkup: LinkupIcon,
loops: LoopsIcon,
luma: LumaIcon,
mailchimp: MailchimpIcon,
mailgun: MailgunIcon,

View File

@@ -0,0 +1,273 @@
---
title: Loops
description: Manage contacts and send emails with Loops
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="loops"
color="#FAFAF9"
/>
{/* MANUAL-CONTENT-START:intro */}
[Loops](https://loops.so/) is an email platform built for modern SaaS companies, offering transactional emails, marketing campaigns, and event-driven automations through a clean API. This integration connects Loops directly into Sim workflows.
With Loops in Sim, you can:
- **Manage contacts**: Create, update, find, and delete contacts in your Loops audience
- **Send transactional emails**: Trigger templated transactional emails with dynamic data variables
- **Fire events**: Send events to Loops to trigger automated email sequences and workflows
- **Manage subscriptions**: Control mailing list subscriptions and contact properties programmatically
- **Enrich contact data**: Attach custom properties, user groups, and mailing list memberships to contacts
In Sim, the Loops integration enables your agents to manage email operations as part of their workflows. Supported operations include:
- **Create Contact**: Add a new contact to your Loops audience with email, name, and custom properties.
- **Update Contact**: Update an existing contact or create one if no match exists (upsert behavior).
- **Find Contact**: Look up a contact by email address or userId.
- **Delete Contact**: Remove a contact from your audience.
- **Send Transactional Email**: Send a templated transactional email to a recipient with dynamic data variables.
- **Send Event**: Trigger a Loops event to start automated email sequences for a contact.
Configure the Loops block with your API key from the Loops dashboard (Settings > API), select an operation, and provide the required parameters. Your agents can then manage contacts and send emails as part of any workflow.
{/* MANUAL-CONTENT-END */}
## Usage Instructions
Integrate Loops into the workflow. Create and manage contacts, send transactional emails, and trigger event-based automations.
## Tools
### `loops_create_contact`
Create a new contact in your Loops audience with an email address and optional properties like name, user group, and mailing list subscriptions.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Loops API key for authentication |
| `email` | string | Yes | The email address for the new contact |
| `firstName` | string | No | The contact first name |
| `lastName` | string | No | The contact last name |
| `source` | string | No | Custom source value replacing the default "API" |
| `subscribed` | boolean | No | Whether the contact receives campaign emails \(defaults to true\) |
| `userGroup` | string | No | Group to segment the contact into \(one group per contact\) |
| `userId` | string | No | Unique user identifier from your application |
| `mailingLists` | json | No | Mailing list IDs mapped to boolean values \(true to subscribe, false to unsubscribe\) |
| `customProperties` | json | No | Custom contact properties as key-value pairs \(string, number, boolean, or date values\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Whether the contact was created successfully |
| `id` | string | The Loops-assigned ID of the created contact |
### `loops_update_contact`
Update an existing contact in Loops by email or userId. Creates a new contact if no match is found (upsert). Can update name, subscription status, user group, mailing lists, and custom properties.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Loops API key for authentication |
| `email` | string | No | The contact email address \(at least one of email or userId is required\) |
| `userId` | string | No | The contact userId \(at least one of email or userId is required\) |
| `firstName` | string | No | The contact first name |
| `lastName` | string | No | The contact last name |
| `source` | string | No | Custom source value replacing the default "API" |
| `subscribed` | boolean | No | Whether the contact receives campaign emails \(sending true re-subscribes unsubscribed contacts\) |
| `userGroup` | string | No | Group to segment the contact into \(one group per contact\) |
| `mailingLists` | json | No | Mailing list IDs mapped to boolean values \(true to subscribe, false to unsubscribe\) |
| `customProperties` | json | No | Custom contact properties as key-value pairs \(send null to reset a property\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Whether the contact was updated successfully |
| `id` | string | The Loops-assigned ID of the updated or created contact |
### `loops_find_contact`
Find a contact in Loops by email address or userId. Returns an array of matching contacts with all their properties including name, subscription status, user group, and mailing lists.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Loops API key for authentication |
| `email` | string | No | The contact email address to search for \(at least one of email or userId is required\) |
| `userId` | string | No | The contact userId to search for \(at least one of email or userId is required\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `contacts` | array | Array of matching contact objects \(empty array if no match found\) |
| ↳ `id` | string | Loops-assigned contact ID |
| ↳ `email` | string | Contact email address |
| ↳ `firstName` | string | Contact first name |
| ↳ `lastName` | string | Contact last name |
| ↳ `source` | string | Source the contact was created from |
| ↳ `subscribed` | boolean | Whether the contact receives campaign emails |
| ↳ `userGroup` | string | Contact user group |
| ↳ `userId` | string | External user identifier |
| ↳ `mailingLists` | object | Mailing list IDs mapped to subscription status |
| ↳ `optInStatus` | string | Double opt-in status: pending, accepted, rejected, or null |
### `loops_delete_contact`
Delete a contact from Loops by email address or userId. At least one identifier must be provided.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Loops API key for authentication |
| `email` | string | No | The email address of the contact to delete \(at least one of email or userId is required\) |
| `userId` | string | No | The userId of the contact to delete \(at least one of email or userId is required\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Whether the contact was deleted successfully |
| `message` | string | Status message from the API |
### `loops_send_transactional_email`
Send a transactional email to a recipient using a Loops template. Supports dynamic data variables for personalization and optionally adds the recipient to your audience.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Loops API key for authentication |
| `email` | string | Yes | The email address of the recipient |
| `transactionalId` | string | Yes | The ID of the transactional email template to send |
| `dataVariables` | json | No | Template data variables as key-value pairs \(string or number values\) |
| `addToAudience` | boolean | No | Whether to create the recipient as a contact if they do not already exist \(default: false\) |
| `attachments` | json | No | Array of file attachments. Each object must have filename \(string\), contentType \(MIME type string\), and data \(base64-encoded string\). |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Whether the transactional email was sent successfully |
### `loops_send_event`
Send an event to Loops to trigger automated email sequences for a contact. Identify the contact by email or userId and include optional event properties and mailing list changes.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Loops API key for authentication |
| `email` | string | No | The email address of the contact \(at least one of email or userId is required\) |
| `userId` | string | No | The userId of the contact \(at least one of email or userId is required\) |
| `eventName` | string | Yes | The name of the event to trigger |
| `eventProperties` | json | No | Event data as key-value pairs \(string, number, boolean, or date values\) |
| `mailingLists` | json | No | Mailing list IDs mapped to boolean values \(true to subscribe, false to unsubscribe\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Whether the event was sent successfully |
### `loops_list_mailing_lists`
Retrieve all mailing lists from your Loops account. Returns each list with its ID, name, description, and public/private status.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Loops API key for authentication |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `mailingLists` | array | Array of mailing list objects |
| ↳ `id` | string | The mailing list ID |
| ↳ `name` | string | The mailing list name |
| ↳ `description` | string | The mailing list description \(null if not set\) |
| ↳ `isPublic` | boolean | Whether the list is public or private |
### `loops_list_transactional_emails`
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.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Loops API key for authentication |
| `perPage` | string | No | Number of results per page \(10-50, default: 20\) |
| `cursor` | string | No | Pagination cursor from a previous response to fetch the next page |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `transactionalEmails` | array | Array of published transactional email templates |
| ↳ `id` | string | The transactional email template ID |
| ↳ `name` | string | The template name |
| ↳ `lastUpdated` | string | Last updated timestamp |
| ↳ `dataVariables` | array | Template data variable names |
| `pagination` | object | Pagination information |
| ↳ `totalResults` | number | Total number of results |
| ↳ `returnedResults` | number | Number of results returned |
| ↳ `perPage` | number | Results per page |
| ↳ `totalPages` | number | Total number of pages |
| ↳ `nextCursor` | string | Cursor for next page \(null if no more pages\) |
| ↳ `nextPage` | string | URL for next page \(null if no more pages\) |
### `loops_create_contact_property`
Create a new custom contact property in your Loops account. The property name must be in camelCase format.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Loops API key for authentication |
| `name` | string | Yes | The property name in camelCase format \(e.g., "favoriteColor"\) |
| `type` | string | Yes | The property data type \(e.g., "string", "number", "boolean", "date"\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Whether the contact property was created successfully |
### `loops_list_contact_properties`
Retrieve a list of contact properties from your Loops account. Returns each property with its key, label, and data type. Can filter to show all properties or only custom ones.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Loops API key for authentication |
| `list` | string | No | Filter type: "all" for all properties \(default\) or "custom" for custom properties only |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `properties` | array | Array of contact property objects |
| ↳ `key` | string | The property key \(camelCase identifier\) |
| ↳ `label` | string | The property display label |
| ↳ `type` | string | The property data type \(string, number, boolean, date\) |

View File

@@ -77,6 +77,7 @@
"linear",
"linkedin",
"linkup",
"loops",
"luma",
"mailchimp",
"mailgun",

View File

@@ -0,0 +1,519 @@
import { LoopsIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import type { LoopsResponse } from '@/tools/loops/types'
export const LoopsBlock: BlockConfig<LoopsResponse> = {
type: 'loops',
name: 'Loops',
description: 'Manage contacts and send emails with Loops',
authMode: AuthMode.ApiKey,
longDescription:
'Integrate Loops into the workflow. Create and manage contacts, send transactional emails, and trigger event-based automations.',
docsLink: 'https://docs.sim.ai/tools/loops',
category: 'tools',
bgColor: '#FAFAF9',
icon: LoopsIcon,
subBlocks: [
{
id: 'operation',
title: 'Operation',
type: 'dropdown',
options: [
{ label: 'Create Contact', id: 'create_contact' },
{ label: 'Update Contact', id: 'update_contact' },
{ label: 'Find Contact', id: 'find_contact' },
{ label: 'Delete Contact', id: 'delete_contact' },
{ label: 'Send Transactional Email', id: 'send_transactional_email' },
{ label: 'Send Event', id: 'send_event' },
{ label: 'List Mailing Lists', id: 'list_mailing_lists' },
{ label: 'List Transactional Emails', id: 'list_transactional_emails' },
{ label: 'Create Contact Property', id: 'create_contact_property' },
{ label: 'List Contact Properties', id: 'list_contact_properties' },
],
value: () => 'create_contact',
},
// Required email for create and send transactional
{
id: 'email',
title: 'Email',
type: 'short-input',
placeholder: 'Enter email address',
required: true,
condition: {
field: 'operation',
value: ['create_contact', 'send_transactional_email'],
},
},
// Optional email for update, find, delete, send event
{
id: 'contactEmail',
title: 'Email',
type: 'short-input',
placeholder: 'Enter email address',
condition: {
field: 'operation',
value: ['update_contact', 'find_contact', 'delete_contact', 'send_event'],
},
},
// User ID for operations that support it
{
id: 'userId',
title: 'User ID',
type: 'short-input',
placeholder: 'Enter user ID',
condition: {
field: 'operation',
value: ['update_contact', 'find_contact', 'delete_contact', 'send_event'],
},
},
// Contact fields
{
id: 'firstName',
title: 'First Name',
type: 'short-input',
placeholder: 'Enter first name',
condition: {
field: 'operation',
value: ['create_contact', 'update_contact'],
},
},
{
id: 'lastName',
title: 'Last Name',
type: 'short-input',
placeholder: 'Enter last name',
condition: {
field: 'operation',
value: ['create_contact', 'update_contact'],
},
},
// Advanced contact fields
{
id: 'source',
title: 'Source',
type: 'short-input',
placeholder: 'Custom source (default: "API")',
condition: {
field: 'operation',
value: ['create_contact', 'update_contact'],
},
mode: 'advanced',
},
{
id: 'subscribed',
title: 'Subscribed',
type: 'switch',
condition: {
field: 'operation',
value: ['create_contact', 'update_contact'],
},
mode: 'advanced',
},
{
id: 'userGroup',
title: 'User Group',
type: 'short-input',
placeholder: 'Enter user group',
condition: {
field: 'operation',
value: ['create_contact', 'update_contact'],
},
mode: 'advanced',
},
{
id: 'createUserId',
title: 'User ID',
type: 'short-input',
placeholder: 'Enter unique user ID',
condition: {
field: 'operation',
value: 'create_contact',
},
mode: 'advanced',
},
{
id: 'mailingLists',
title: 'Mailing Lists',
type: 'long-input',
placeholder: '{"listId123": true, "listId456": false}',
condition: {
field: 'operation',
value: ['create_contact', 'update_contact', 'send_event'],
},
mode: 'advanced',
wandConfig: {
enabled: true,
prompt: `Generate a JSON object mapping Loops mailing list IDs to boolean values. Use true to subscribe the contact to a list and false to unsubscribe.
Current value: {context}
The output must be a valid JSON object with string keys (mailing list IDs) and boolean values.
Example:
{
"clxf1nxlb000t0ml79ajwcsj0": true,
"clxf2q43u00010mlh12q9ggx1": false
}`,
placeholder: 'Describe the mailing list subscriptions...',
},
},
{
id: 'customProperties',
title: 'Custom Properties',
type: 'long-input',
placeholder: '{"plan": "pro", "company": "Acme"}',
condition: {
field: 'operation',
value: ['create_contact', 'update_contact'],
},
mode: 'advanced',
wandConfig: {
enabled: true,
prompt: `Generate a JSON object of custom contact properties for Loops. Values can be strings, numbers, booleans, or ISO 8601 date strings. Send null to reset a property.
Current value: {context}
The output must be a valid JSON object.
Example:
{
"plan": "pro",
"company": "Acme Inc",
"signupDate": "2024-01-15T00:00:00Z",
"isActive": true,
"seats": 5
}`,
placeholder: 'Describe the custom properties...',
},
},
// Transactional email fields
{
id: 'transactionalId',
title: 'Transactional Email ID',
type: 'short-input',
placeholder: 'Enter template ID (e.g., clx...)',
required: { field: 'operation', value: 'send_transactional_email' },
condition: {
field: 'operation',
value: 'send_transactional_email',
},
},
{
id: 'dataVariables',
title: 'Data Variables',
type: 'long-input',
placeholder: '{"name": "John", "url": "https://..."}',
condition: {
field: 'operation',
value: 'send_transactional_email',
},
wandConfig: {
enabled: true,
prompt: `Generate a JSON object of data variables for a Loops transactional email template. Values must be strings or numbers, matching the variable names defined in the template.
Current value: {context}
The output must be a valid JSON object with string keys.
Example:
{
"name": "John Smith",
"confirmationUrl": "https://example.com/confirm?token=abc123",
"expiresIn": 24
}`,
placeholder: 'Describe the template variables...',
},
},
{
id: 'addToAudience',
title: 'Add to Audience',
type: 'switch',
condition: {
field: 'operation',
value: 'send_transactional_email',
},
mode: 'advanced',
},
{
id: 'attachments',
title: 'Attachments',
type: 'long-input',
placeholder:
'[{"filename": "file.pdf", "contentType": "application/pdf", "data": "base64..."}]',
condition: {
field: 'operation',
value: 'send_transactional_email',
},
mode: 'advanced',
wandConfig: {
enabled: true,
prompt: `Generate a JSON array of file attachments for a Loops transactional email. Each object must have: filename (string), contentType (MIME type string), and data (base64-encoded file content string).
Current value: {context}
The output must be a valid JSON array.
Example:
[
{
"filename": "invoice.pdf",
"contentType": "application/pdf",
"data": "JVBERi0xLjQK..."
}
]`,
placeholder: 'Describe the attachments...',
},
},
// Event fields
{
id: 'eventName',
title: 'Event Name',
type: 'short-input',
placeholder: 'Enter event name (e.g., signup_completed)',
required: { field: 'operation', value: 'send_event' },
condition: {
field: 'operation',
value: 'send_event',
},
},
{
id: 'eventProperties',
title: 'Event Properties',
type: 'long-input',
placeholder: '{"plan": "pro", "amount": 49.99}',
condition: {
field: 'operation',
value: 'send_event',
},
wandConfig: {
enabled: true,
prompt: `Generate a JSON object of event properties for a Loops event. Values can be strings, numbers, booleans, or ISO 8601 date strings.
Current value: {context}
The output must be a valid JSON object.
Example:
{
"plan": "pro",
"amount": 49.99,
"currency": "USD",
"isUpgrade": true
}`,
placeholder: 'Describe the event properties...',
},
},
// List transactional emails pagination fields
{
id: 'perPage',
title: 'Results Per Page',
type: 'short-input',
placeholder: '20 (range: 10-50)',
condition: {
field: 'operation',
value: 'list_transactional_emails',
},
mode: 'advanced',
},
{
id: 'cursor',
title: 'Pagination Cursor',
type: 'short-input',
placeholder: 'Cursor from previous response',
condition: {
field: 'operation',
value: 'list_transactional_emails',
},
mode: 'advanced',
},
// Create contact property fields
{
id: 'propertyName',
title: 'Property Name',
type: 'short-input',
placeholder: 'Enter property name in camelCase (e.g., favoriteColor)',
required: { field: 'operation', value: 'create_contact_property' },
condition: {
field: 'operation',
value: 'create_contact_property',
},
},
{
id: 'propertyType',
title: 'Property Type',
type: 'dropdown',
options: [
{ label: 'String', id: 'string' },
{ label: 'Number', id: 'number' },
{ label: 'Boolean', id: 'boolean' },
{ label: 'Date', id: 'date' },
],
condition: {
field: 'operation',
value: 'create_contact_property',
},
},
// List contact properties filter
{
id: 'propertyFilter',
title: 'Filter',
type: 'dropdown',
options: [
{ label: 'All Properties', id: 'all' },
{ label: 'Custom Only', id: 'custom' },
],
condition: {
field: 'operation',
value: 'list_contact_properties',
},
mode: 'advanced',
},
// API Key (always visible)
{
id: 'apiKey',
title: 'API Key',
type: 'short-input',
placeholder: 'Enter your Loops API key',
password: true,
required: true,
},
],
tools: {
access: [
'loops_create_contact',
'loops_update_contact',
'loops_find_contact',
'loops_delete_contact',
'loops_send_transactional_email',
'loops_send_event',
'loops_list_mailing_lists',
'loops_list_transactional_emails',
'loops_create_contact_property',
'loops_list_contact_properties',
],
config: {
tool: (params) => `loops_${params.operation}`,
params: (params) => {
const { operation, apiKey } = params
const result: Record<string, unknown> = { apiKey }
switch (operation) {
case 'create_contact':
result.email = params.email
if (params.firstName) result.firstName = params.firstName
if (params.lastName) result.lastName = params.lastName
if (params.source) result.source = params.source
if (params.subscribed != null) result.subscribed = params.subscribed
if (params.userGroup) result.userGroup = params.userGroup
if (params.createUserId) result.userId = params.createUserId
if (params.mailingLists) result.mailingLists = params.mailingLists
if (params.customProperties) result.customProperties = params.customProperties
break
case 'update_contact':
if (params.contactEmail) result.email = params.contactEmail
if (params.userId) result.userId = params.userId
if (params.firstName) result.firstName = params.firstName
if (params.lastName) result.lastName = params.lastName
if (params.source) result.source = params.source
if (params.subscribed != null) result.subscribed = params.subscribed
if (params.userGroup) result.userGroup = params.userGroup
if (params.mailingLists) result.mailingLists = params.mailingLists
if (params.customProperties) result.customProperties = params.customProperties
break
case 'find_contact':
if (params.contactEmail) result.email = params.contactEmail
if (params.userId) result.userId = params.userId
break
case 'delete_contact':
if (params.contactEmail) result.email = params.contactEmail
if (params.userId) result.userId = params.userId
break
case 'send_transactional_email':
result.email = params.email
result.transactionalId = params.transactionalId
if (params.dataVariables) result.dataVariables = params.dataVariables
if (params.addToAudience != null) result.addToAudience = params.addToAudience
if (params.attachments) result.attachments = params.attachments
break
case 'send_event':
if (params.contactEmail) result.email = params.contactEmail
if (params.userId) result.userId = params.userId
result.eventName = params.eventName
if (params.eventProperties) result.eventProperties = params.eventProperties
if (params.mailingLists) result.mailingLists = params.mailingLists
break
case 'list_transactional_emails':
if (params.perPage) result.perPage = params.perPage
if (params.cursor) result.cursor = params.cursor
break
case 'create_contact_property':
result.name = params.propertyName
result.type = params.propertyType
break
case 'list_contact_properties':
if (params.propertyFilter) result.list = params.propertyFilter
break
}
return result
},
},
},
inputs: {
operation: { type: 'string', description: 'Operation to perform' },
email: { type: 'string', description: 'Contact email address' },
contactEmail: { type: 'string', description: 'Contact email for lookup operations' },
userId: { type: 'string', description: 'Contact user ID' },
firstName: { type: 'string', description: 'Contact first name' },
lastName: { type: 'string', description: 'Contact last name' },
source: { type: 'string', description: 'Contact source' },
subscribed: { type: 'boolean', description: 'Subscription status' },
userGroup: { type: 'string', description: 'Contact user group' },
createUserId: { type: 'string', description: 'User ID for new contact' },
mailingLists: { type: 'json', description: 'Mailing list subscriptions' },
customProperties: { type: 'json', description: 'Custom contact properties' },
transactionalId: { type: 'string', description: 'Transactional email template ID' },
dataVariables: { type: 'json', description: 'Template data variables' },
addToAudience: { type: 'boolean', description: 'Add recipient to audience' },
attachments: { type: 'json', description: 'Email file attachments' },
eventName: { type: 'string', description: 'Event name' },
eventProperties: { type: 'json', description: 'Event properties' },
perPage: { type: 'string', description: 'Results per page for pagination' },
cursor: { type: 'string', description: 'Pagination cursor' },
propertyName: { type: 'string', description: 'Contact property name (camelCase)' },
propertyType: { type: 'string', description: 'Contact property data type' },
propertyFilter: { type: 'string', description: 'Filter for listing properties' },
apiKey: { type: 'string', description: 'Loops API key' },
},
outputs: {
success: { type: 'boolean', description: 'Whether the operation succeeded' },
id: { type: 'string', description: 'Contact ID (create/update operations)' },
contacts: { type: 'json', description: 'Array of matching contacts (find operation)' },
message: { type: 'string', description: 'Status message (delete operation)' },
mailingLists: {
type: 'json',
description: 'Array of mailing lists (list mailing lists operation)',
},
transactionalEmails: {
type: 'json',
description: 'Array of transactional email templates (list transactional emails operation)',
},
pagination: {
type: 'json',
description: 'Pagination info (list transactional emails operation)',
},
properties: {
type: 'json',
description: 'Array of contact properties (list contact properties operation)',
},
},
}

View File

@@ -85,6 +85,7 @@ import { LemlistBlock } from '@/blocks/blocks/lemlist'
import { LinearBlock } from '@/blocks/blocks/linear'
import { LinkedInBlock } from '@/blocks/blocks/linkedin'
import { LinkupBlock } from '@/blocks/blocks/linkup'
import { LoopsBlock } from '@/blocks/blocks/loops'
import { LumaBlock } from '@/blocks/blocks/luma'
import { MailchimpBlock } from '@/blocks/blocks/mailchimp'
import { MailgunBlock } from '@/blocks/blocks/mailgun'
@@ -284,6 +285,7 @@ export const registry: Record<string, BlockConfig> = {
linear: LinearBlock,
linkedin: LinkedInBlock,
linkup: LinkupBlock,
loops: LoopsBlock,
luma: LumaBlock,
mailchimp: MailchimpBlock,
mailgun: MailgunBlock,

View File

@@ -3980,6 +3980,17 @@ export function IntercomIcon(props: SVGProps<SVGSVGElement>) {
)
}
export function LoopsIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg {...props} viewBox='0 0 256 256' fill='none' xmlns='http://www.w3.org/2000/svg'>
<path
fill='currentColor'
d='M192.352 88.042c0-7.012-5.685-12.697-12.697-12.697s-12.697 5.685-12.697 12.697c0 .634.052 1.255.142 1.866a25.248 25.248 0 0 0-4.9-.49c-14.006 0-25.36 11.354-25.36 25.36 0 1.63.16 3.222.456 4.765a37.8 37.8 0 0 0-9.296-1.173c-20.95 0-37.935 16.985-37.935 37.935S107.05 194.24 128 194.24s37.935-16.985 37.935-37.935a37.7 37.7 0 0 0-3.78-16.555 25.2 25.2 0 0 0 12.487-3.336 25.2 25.2 0 0 0 4.558 3.336v.02c14.006 0 25.36-11.354 25.36-25.36 0-12.48-9.018-22.855-20.888-24.996a12.6 12.6 0 0 0 8.68-11.972m-77.05 68.263c0-7.012 5.685-12.697 12.697-12.697s12.697 5.685 12.697 12.697c0 7.013-5.685 12.697-12.697 12.697s-12.697-5.685-12.697-12.697'
/>
</svg>
)
}
export function LumaIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg {...props} fill='none' viewBox='0 0 133 134' xmlns='http://www.w3.org/2000/svg'>

View File

@@ -0,0 +1,148 @@
import type { LoopsCreateContactParams, LoopsCreateContactResponse } from '@/tools/loops/types'
import type { ToolConfig } from '@/tools/types'
export const loopsCreateContactTool: ToolConfig<
LoopsCreateContactParams,
LoopsCreateContactResponse
> = {
id: 'loops_create_contact',
name: 'Loops Create Contact',
description:
'Create a new contact in your Loops audience with an email address and optional properties like name, user group, and mailing list subscriptions.',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Loops API key for authentication',
},
email: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The email address for the new contact',
},
firstName: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'The contact first name',
},
lastName: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'The contact last name',
},
source: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Custom source value replacing the default "API"',
},
subscribed: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Whether the contact receives campaign emails (defaults to true)',
},
userGroup: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Group to segment the contact into (one group per contact)',
},
userId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Unique user identifier from your application',
},
mailingLists: {
type: 'json',
required: false,
visibility: 'user-or-llm',
description:
'Mailing list IDs mapped to boolean values (true to subscribe, false to unsubscribe)',
},
customProperties: {
type: 'json',
required: false,
visibility: 'user-or-llm',
description:
'Custom contact properties as key-value pairs (string, number, boolean, or date values)',
},
},
request: {
url: 'https://app.loops.so/api/v1/contacts/create',
method: 'POST',
headers: (params) => ({
'Content-Type': 'application/json',
Authorization: `Bearer ${params.apiKey}`,
}),
body: (params) => {
// Apply custom properties first so standard fields always take precedence
const body: Record<string, unknown> = {}
if (params.customProperties) {
const props =
typeof params.customProperties === 'string'
? JSON.parse(params.customProperties)
: params.customProperties
Object.assign(body, props)
}
body.email = params.email
if (params.firstName) body.firstName = params.firstName
if (params.lastName) body.lastName = params.lastName
if (params.source) body.source = params.source
if (params.subscribed != null) body.subscribed = params.subscribed
if (params.userGroup) body.userGroup = params.userGroup
if (params.userId) body.userId = params.userId
if (params.mailingLists) {
body.mailingLists =
typeof params.mailingLists === 'string'
? JSON.parse(params.mailingLists)
: params.mailingLists
}
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
return {
success: false,
output: {
success: false,
id: null,
},
error: data.message ?? 'Failed to create contact',
}
}
return {
success: true,
output: {
success: true,
id: data.id ?? null,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Whether the contact was created successfully' },
id: {
type: 'string',
description: 'The Loops-assigned ID of the created contact',
optional: true,
},
},
}

View File

@@ -0,0 +1,78 @@
import type {
LoopsCreateContactPropertyParams,
LoopsCreateContactPropertyResponse,
} from '@/tools/loops/types'
import type { ToolConfig } from '@/tools/types'
export const loopsCreateContactPropertyTool: ToolConfig<
LoopsCreateContactPropertyParams,
LoopsCreateContactPropertyResponse
> = {
id: 'loops_create_contact_property',
name: 'Loops Create Contact Property',
description:
'Create a new custom contact property in your Loops account. The property name must be in camelCase format.',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Loops API key for authentication',
},
name: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The property name in camelCase format (e.g., "favoriteColor")',
},
type: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The property data type (e.g., "string", "number", "boolean", "date")',
},
},
request: {
url: 'https://app.loops.so/api/v1/contacts/properties',
method: 'POST',
headers: (params) => ({
'Content-Type': 'application/json',
Authorization: `Bearer ${params.apiKey}`,
}),
body: (params) => ({
name: params.name,
type: params.type,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
return {
success: false,
output: {
success: false,
},
error: data.message ?? 'Failed to create contact property',
}
}
return {
success: true,
output: {
success: true,
},
}
},
outputs: {
success: {
type: 'boolean',
description: 'Whether the contact property was created successfully',
},
},
}

View File

@@ -0,0 +1,82 @@
import type { LoopsDeleteContactParams, LoopsDeleteContactResponse } from '@/tools/loops/types'
import type { ToolConfig } from '@/tools/types'
export const loopsDeleteContactTool: ToolConfig<
LoopsDeleteContactParams,
LoopsDeleteContactResponse
> = {
id: 'loops_delete_contact',
name: 'Loops Delete Contact',
description:
'Delete a contact from Loops by email address or userId. At least one identifier must be provided.',
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 email address of the contact to delete (at least one of email or userId is required)',
},
userId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'The userId of the contact to delete (at least one of email or userId is required)',
},
},
request: {
url: 'https://app.loops.so/api/v1/contacts/delete',
method: 'POST',
headers: (params) => ({
'Content-Type': 'application/json',
Authorization: `Bearer ${params.apiKey}`,
}),
body: (params) => {
if (!params.email && !params.userId) {
throw new Error('At least one of email or userId is required to delete a contact')
}
const body: Record<string, unknown> = {}
if (params.email) body.email = params.email
if (params.userId) body.userId = params.userId
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
return {
success: false,
output: {
success: false,
message: data.message ?? 'Failed to delete contact',
},
error: data.message ?? 'Failed to delete contact',
}
}
return {
success: true,
output: {
success: true,
message: data.message ?? 'Contact deleted.',
},
}
},
outputs: {
success: { type: 'boolean', description: 'Whether the contact was deleted successfully' },
message: { type: 'string', description: 'Status message from the API' },
},
}

View File

@@ -0,0 +1,91 @@
import type { LoopsFindContactParams, LoopsFindContactResponse } from '@/tools/loops/types'
import { LOOPS_CONTACT_OUTPUT_PROPERTIES } from '@/tools/loops/types'
import type { ToolConfig } from '@/tools/types'
export const loopsFindContactTool: ToolConfig<LoopsFindContactParams, LoopsFindContactResponse> = {
id: 'loops_find_contact',
name: 'Loops Find Contact',
description:
'Find a contact in Loops by email address or userId. Returns an array of matching contacts with all their properties including name, subscription status, user group, and mailing lists.',
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 search for (at least one of email or userId is required)',
},
userId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'The contact userId to search for (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 find a contact')
}
const base = 'https://app.loops.so/api/v1/contacts/find'
if (params.email) return `${base}?email=${encodeURIComponent(params.email)}`
return `${base}?userId=${encodeURIComponent(params.userId!)}`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!Array.isArray(data)) {
return {
success: false,
output: {
contacts: [],
},
error: data.message ?? 'Failed to find contact',
}
}
return {
success: true,
output: {
contacts: data.map((contact: Record<string, unknown>) => ({
id: (contact.id as string) ?? '',
email: (contact.email as string) ?? '',
firstName: (contact.firstName as string) ?? null,
lastName: (contact.lastName as string) ?? null,
source: (contact.source as string) ?? null,
subscribed: (contact.subscribed as boolean) ?? false,
userGroup: (contact.userGroup as string) ?? null,
userId: (contact.userId as string) ?? null,
mailingLists: (contact.mailingLists as Record<string, boolean>) ?? {},
optInStatus: (contact.optInStatus as string) ?? null,
})),
},
}
},
outputs: {
contacts: {
type: 'array',
description: 'Array of matching contact objects (empty array if no match found)',
items: {
type: 'object',
properties: LOOPS_CONTACT_OUTPUT_PROPERTIES,
},
},
},
}

View File

@@ -0,0 +1,10 @@
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 { loopsListContactPropertiesTool } from '@/tools/loops/list_contact_properties'
export { loopsListMailingListsTool } from '@/tools/loops/list_mailing_lists'
export { loopsListTransactionalEmailsTool } from '@/tools/loops/list_transactional_emails'
export { loopsSendEventTool } from '@/tools/loops/send_event'
export { loopsSendTransactionalEmailTool } from '@/tools/loops/send_transactional_email'
export { loopsUpdateContactTool } from '@/tools/loops/update_contact'

View File

@@ -0,0 +1,87 @@
import type {
LoopsListContactPropertiesParams,
LoopsListContactPropertiesResponse,
} from '@/tools/loops/types'
import type { ToolConfig } from '@/tools/types'
export const loopsListContactPropertiesTool: ToolConfig<
LoopsListContactPropertiesParams,
LoopsListContactPropertiesResponse
> = {
id: 'loops_list_contact_properties',
name: 'Loops List Contact Properties',
description:
'Retrieve a list of contact properties from your Loops account. Returns each property with its key, label, and data type. Can filter to show all properties or only custom ones.',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Loops API key for authentication',
},
list: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Filter type: "all" for all properties (default) or "custom" for custom properties only',
},
},
request: {
url: (params) => {
const base = 'https://app.loops.so/api/v1/contacts/properties'
if (params.list) return `${base}?list=${encodeURIComponent(params.list)}`
return base
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!Array.isArray(data)) {
return {
success: false,
output: {
properties: [],
},
error: data.message ?? 'Failed to list contact properties',
}
}
return {
success: true,
output: {
properties: data.map((prop: Record<string, unknown>) => ({
key: (prop.key as string) ?? '',
label: (prop.label as string) ?? '',
type: (prop.type as string) ?? '',
})),
},
}
},
outputs: {
properties: {
type: 'array',
description: 'Array of contact property objects',
items: {
type: 'object',
properties: {
key: { type: 'string', description: 'The property key (camelCase identifier)' },
label: { type: 'string', description: 'The property display label' },
type: {
type: 'string',
description: 'The property data type (string, number, boolean, date)',
},
},
},
},
},
}

View File

@@ -0,0 +1,82 @@
import type {
LoopsListMailingListsParams,
LoopsListMailingListsResponse,
} from '@/tools/loops/types'
import type { ToolConfig } from '@/tools/types'
export const loopsListMailingListsTool: ToolConfig<
LoopsListMailingListsParams,
LoopsListMailingListsResponse
> = {
id: 'loops_list_mailing_lists',
name: 'Loops List Mailing Lists',
description:
'Retrieve all mailing lists from your Loops account. Returns each list with its ID, name, description, and public/private status.',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Loops API key for authentication',
},
},
request: {
url: 'https://app.loops.so/api/v1/lists',
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!Array.isArray(data)) {
return {
success: false,
output: {
mailingLists: [],
},
error: data.message ?? 'Failed to list mailing lists',
}
}
return {
success: true,
output: {
mailingLists: data.map((list: Record<string, unknown>) => ({
id: (list.id as string) ?? '',
name: (list.name as string) ?? '',
description: (list.description as string) ?? null,
isPublic: (list.isPublic as boolean) ?? false,
})),
},
}
},
outputs: {
mailingLists: {
type: 'array',
description: 'Array of mailing list objects',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'The mailing list ID' },
name: { type: 'string', description: 'The mailing list name' },
description: {
type: 'string',
description: 'The mailing list description (null if not set)',
optional: true,
},
isPublic: {
type: 'boolean',
description: 'Whether the list is public or private',
},
},
},
},
},
}

View File

@@ -0,0 +1,135 @@
import type {
LoopsListTransactionalEmailsParams,
LoopsListTransactionalEmailsResponse,
} from '@/tools/loops/types'
import type { ToolConfig } from '@/tools/types'
export const loopsListTransactionalEmailsTool: ToolConfig<
LoopsListTransactionalEmailsParams,
LoopsListTransactionalEmailsResponse
> = {
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.',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Loops API key for authentication',
},
perPage: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Number of results per page (10-50, default: 20)',
},
cursor: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Pagination cursor from a previous response to fetch the next page',
},
},
request: {
url: (params) => {
const base = 'https://app.loops.so/api/v1/transactional'
const queryParams: string[] = []
if (params.perPage) queryParams.push(`perPage=${encodeURIComponent(params.perPage)}`)
if (params.cursor) queryParams.push(`cursor=${encodeURIComponent(params.cursor)}`)
return queryParams.length > 0 ? `${base}?${queryParams.join('&')}` : base
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.data && !Array.isArray(data)) {
return {
success: false,
output: {
transactionalEmails: [],
pagination: {
totalResults: 0,
returnedResults: 0,
perPage: 0,
totalPages: 0,
nextCursor: null,
nextPage: null,
},
},
error: data.message ?? 'Failed to list transactional emails',
}
}
const emails = data.data ?? data ?? []
return {
success: true,
output: {
transactionalEmails: emails.map((email: Record<string, unknown>) => ({
id: (email.id as string) ?? '',
name: (email.name as string) ?? '',
lastUpdated: (email.lastUpdated as string) ?? '',
dataVariables: (email.dataVariables as string[]) ?? [],
})),
pagination: {
totalResults: (data.pagination?.totalResults as number) ?? emails.length,
returnedResults: (data.pagination?.returnedResults as number) ?? emails.length,
perPage: (data.pagination?.perPage as number) ?? 20,
totalPages: (data.pagination?.totalPages as number) ?? 1,
nextCursor: (data.pagination?.nextCursor as string) ?? null,
nextPage: (data.pagination?.nextPage as string) ?? null,
},
},
}
},
outputs: {
transactionalEmails: {
type: 'array',
description: 'Array of published transactional email templates',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'The transactional email template ID' },
name: { type: 'string', description: 'The template name' },
lastUpdated: { type: 'string', description: 'Last updated timestamp' },
dataVariables: {
type: 'array',
description: 'Template data variable names',
items: { type: 'string' },
},
},
},
},
pagination: {
type: 'object',
description: 'Pagination information',
properties: {
totalResults: { type: 'number', description: 'Total number of results' },
returnedResults: { type: 'number', description: 'Number of results returned' },
perPage: { type: 'number', description: 'Results per page' },
totalPages: { type: 'number', description: 'Total number of pages' },
nextCursor: {
type: 'string',
description: 'Cursor for next page (null if no more pages)',
optional: true,
},
nextPage: {
type: 'string',
description: 'URL for next page (null if no more pages)',
optional: true,
},
},
},
},
}

View File

@@ -0,0 +1,112 @@
import type { LoopsSendEventParams, LoopsSendEventResponse } from '@/tools/loops/types'
import type { ToolConfig } from '@/tools/types'
export const loopsSendEventTool: ToolConfig<LoopsSendEventParams, LoopsSendEventResponse> = {
id: 'loops_send_event',
name: 'Loops Send Event',
description:
'Send an event to Loops to trigger automated email sequences for a contact. Identify the contact by email or userId and include optional event properties and mailing list changes.',
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 email address of the contact (at least one of email or userId is required)',
},
userId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'The userId of the contact (at least one of email or userId is required)',
},
eventName: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The name of the event to trigger',
},
eventProperties: {
type: 'json',
required: false,
visibility: 'user-or-llm',
description: 'Event data as key-value pairs (string, number, boolean, or date values)',
},
mailingLists: {
type: 'json',
required: false,
visibility: 'user-or-llm',
description:
'Mailing list IDs mapped to boolean values (true to subscribe, false to unsubscribe)',
},
},
request: {
url: 'https://app.loops.so/api/v1/events/send',
method: 'POST',
headers: (params) => ({
'Content-Type': 'application/json',
Authorization: `Bearer ${params.apiKey}`,
}),
body: (params) => {
if (!params.email && !params.userId) {
throw new Error('At least one of email or userId is required to send an event')
}
const body: Record<string, unknown> = {
eventName: params.eventName,
}
if (params.email) body.email = params.email
if (params.userId) body.userId = params.userId
if (params.eventProperties) {
body.eventProperties =
typeof params.eventProperties === 'string'
? JSON.parse(params.eventProperties)
: params.eventProperties
}
if (params.mailingLists) {
body.mailingLists =
typeof params.mailingLists === 'string'
? JSON.parse(params.mailingLists)
: params.mailingLists
}
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
return {
success: false,
output: {
success: false,
},
error: data.message ?? 'Failed to send event',
}
}
return {
success: true,
output: {
success: true,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Whether the event was sent successfully' },
},
}

View File

@@ -0,0 +1,120 @@
import type {
LoopsSendTransactionalEmailParams,
LoopsSendTransactionalEmailResponse,
} from '@/tools/loops/types'
import type { ToolConfig } from '@/tools/types'
export const loopsSendTransactionalEmailTool: ToolConfig<
LoopsSendTransactionalEmailParams,
LoopsSendTransactionalEmailResponse
> = {
id: 'loops_send_transactional_email',
name: 'Loops Send Transactional Email',
description:
'Send a transactional email to a recipient using a Loops template. Supports dynamic data variables for personalization and optionally adds the recipient to your audience.',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Loops API key for authentication',
},
email: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The email address of the recipient',
},
transactionalId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The ID of the transactional email template to send',
},
dataVariables: {
type: 'json',
required: false,
visibility: 'user-or-llm',
description: 'Template data variables as key-value pairs (string or number values)',
},
addToAudience: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description:
'Whether to create the recipient as a contact if they do not already exist (default: false)',
},
attachments: {
type: 'json',
required: false,
visibility: 'user-or-llm',
description:
'Array of file attachments. Each object must have filename (string), contentType (MIME type string), and data (base64-encoded string).',
},
},
request: {
url: 'https://app.loops.so/api/v1/transactional',
method: 'POST',
headers: (params) => ({
'Content-Type': 'application/json',
Authorization: `Bearer ${params.apiKey}`,
}),
body: (params) => {
const body: Record<string, unknown> = {
email: params.email,
transactionalId: params.transactionalId,
}
if (params.dataVariables) {
body.dataVariables =
typeof params.dataVariables === 'string'
? JSON.parse(params.dataVariables)
: params.dataVariables
}
if (params.addToAudience != null) {
body.addToAudience = params.addToAudience
}
if (params.attachments) {
body.attachments =
typeof params.attachments === 'string'
? JSON.parse(params.attachments)
: params.attachments
}
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
return {
success: false,
output: {
success: false,
},
error: data.message ?? 'Failed to send transactional email',
}
}
return {
success: true,
output: {
success: true,
},
}
},
outputs: {
success: {
type: 'boolean',
description: 'Whether the transactional email was sent successfully',
},
},
}

View File

@@ -0,0 +1,209 @@
import type { ToolResponse } from '@/tools/types'
export interface LoopsBaseParams {
apiKey: string
}
export interface LoopsCreateContactParams extends LoopsBaseParams {
email: string
firstName?: string
lastName?: string
source?: string
subscribed?: boolean
userGroup?: string
userId?: string
mailingLists?: string | Record<string, boolean>
customProperties?: string | Record<string, unknown>
}
export interface LoopsUpdateContactParams extends LoopsBaseParams {
email?: string
userId?: string
firstName?: string
lastName?: string
source?: string
subscribed?: boolean
userGroup?: string
mailingLists?: string | Record<string, boolean>
customProperties?: string | Record<string, unknown>
}
export interface LoopsFindContactParams extends LoopsBaseParams {
email?: string
userId?: string
}
export interface LoopsDeleteContactParams extends LoopsBaseParams {
email?: string
userId?: string
}
export interface LoopsSendTransactionalEmailParams extends LoopsBaseParams {
email: string
transactionalId: string
dataVariables?: string | Record<string, string | number>
addToAudience?: boolean
attachments?: string | { filename: string; contentType: string; data: string }[]
}
export interface LoopsSendEventParams extends LoopsBaseParams {
email?: string
userId?: string
eventName: string
eventProperties?: string | Record<string, string | number | boolean>
mailingLists?: string | Record<string, boolean>
}
export interface LoopsListMailingListsParams extends LoopsBaseParams {}
export interface LoopsListTransactionalEmailsParams extends LoopsBaseParams {
perPage?: string
cursor?: string
}
export interface LoopsCreateContactPropertyParams extends LoopsBaseParams {
name: string
type: string
}
export interface LoopsListContactPropertiesParams extends LoopsBaseParams {
list?: string
}
export interface LoopsContact {
id: string
email: string
firstName: string | null
lastName: string | null
source: string | null
subscribed: boolean
userGroup: string | null
userId: string | null
mailingLists: Record<string, boolean>
optInStatus: string | null
}
export interface LoopsCreateContactResponse extends ToolResponse {
output: {
success: boolean
id: string | null
}
}
export interface LoopsUpdateContactResponse extends ToolResponse {
output: {
success: boolean
id: string | null
}
}
export interface LoopsFindContactResponse extends ToolResponse {
output: {
contacts: LoopsContact[]
}
}
export interface LoopsDeleteContactResponse extends ToolResponse {
output: {
success: boolean
message: string | null
}
}
export interface LoopsSendTransactionalEmailResponse extends ToolResponse {
output: {
success: boolean
}
}
export interface LoopsSendEventResponse extends ToolResponse {
output: {
success: boolean
}
}
export interface LoopsListMailingListsResponse extends ToolResponse {
output: {
mailingLists: {
id: string
name: string
description: string | null
isPublic: boolean
}[]
}
}
export interface LoopsListTransactionalEmailsResponse extends ToolResponse {
output: {
transactionalEmails: {
id: string
name: string
lastUpdated: string
dataVariables: string[]
}[]
pagination: {
totalResults: number
returnedResults: number
perPage: number
totalPages: number
nextCursor: string | null
nextPage: string | null
}
}
}
export interface LoopsCreateContactPropertyResponse extends ToolResponse {
output: {
success: boolean
}
}
export interface LoopsListContactPropertiesResponse extends ToolResponse {
output: {
properties: {
key: string
label: string
type: string
}[]
}
}
export type LoopsResponse =
| LoopsCreateContactResponse
| LoopsUpdateContactResponse
| LoopsFindContactResponse
| LoopsDeleteContactResponse
| LoopsSendTransactionalEmailResponse
| LoopsSendEventResponse
| LoopsListMailingListsResponse
| LoopsListTransactionalEmailsResponse
| LoopsCreateContactPropertyResponse
| LoopsListContactPropertiesResponse
export const LOOPS_CONTACT_OUTPUT_PROPERTIES = {
id: { type: 'string' as const, description: 'Loops-assigned contact ID' },
email: { type: 'string' as const, description: 'Contact email address' },
firstName: { type: 'string' as const, description: 'Contact first name', optional: true },
lastName: { type: 'string' as const, description: 'Contact last name', optional: true },
source: {
type: 'string' as const,
description: 'Source the contact was created from',
optional: true,
},
subscribed: {
type: 'boolean' as const,
description: 'Whether the contact receives campaign emails',
},
userGroup: { type: 'string' as const, description: 'Contact user group', optional: true },
userId: { type: 'string' as const, description: 'External user identifier', optional: true },
mailingLists: {
type: 'object' as const,
description: 'Mailing list IDs mapped to subscription status',
optional: true,
},
optInStatus: {
type: 'string' as const,
description: 'Double opt-in status: pending, accepted, rejected, or null',
optional: true,
},
}

View File

@@ -0,0 +1,152 @@
import type { LoopsUpdateContactParams, LoopsUpdateContactResponse } from '@/tools/loops/types'
import type { ToolConfig } from '@/tools/types'
export const loopsUpdateContactTool: ToolConfig<
LoopsUpdateContactParams,
LoopsUpdateContactResponse
> = {
id: 'loops_update_contact',
name: 'Loops Update Contact',
description:
'Update an existing contact in Loops by email or userId. Creates a new contact if no match is found (upsert). Can update name, subscription status, user group, mailing lists, and custom properties.',
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 (at least one of email or userId is required)',
},
userId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'The contact userId (at least one of email or userId is required)',
},
firstName: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'The contact first name',
},
lastName: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'The contact last name',
},
source: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Custom source value replacing the default "API"',
},
subscribed: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description:
'Whether the contact receives campaign emails (sending true re-subscribes unsubscribed contacts)',
},
userGroup: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Group to segment the contact into (one group per contact)',
},
mailingLists: {
type: 'json',
required: false,
visibility: 'user-or-llm',
description:
'Mailing list IDs mapped to boolean values (true to subscribe, false to unsubscribe)',
},
customProperties: {
type: 'json',
required: false,
visibility: 'user-or-llm',
description: 'Custom contact properties as key-value pairs (send null to reset a property)',
},
},
request: {
url: 'https://app.loops.so/api/v1/contacts/update',
method: 'PUT',
headers: (params) => ({
'Content-Type': 'application/json',
Authorization: `Bearer ${params.apiKey}`,
}),
body: (params) => {
if (!params.email && !params.userId) {
throw new Error('At least one of email or userId is required to update a contact')
}
// Apply custom properties first so standard fields always take precedence
const body: Record<string, unknown> = {}
if (params.customProperties) {
const props =
typeof params.customProperties === 'string'
? JSON.parse(params.customProperties)
: params.customProperties
Object.assign(body, props)
}
if (params.email) body.email = params.email
if (params.userId) body.userId = params.userId
if (params.firstName) body.firstName = params.firstName
if (params.lastName) body.lastName = params.lastName
if (params.source) body.source = params.source
if (params.subscribed != null) body.subscribed = params.subscribed
if (params.userGroup) body.userGroup = params.userGroup
if (params.mailingLists) {
body.mailingLists =
typeof params.mailingLists === 'string'
? JSON.parse(params.mailingLists)
: params.mailingLists
}
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
return {
success: false,
output: {
success: false,
id: null,
},
error: data.message ?? 'Failed to update contact',
}
}
return {
success: true,
output: {
success: true,
id: data.id ?? null,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Whether the contact was updated successfully' },
id: {
type: 'string',
description: 'The Loops-assigned ID of the updated or created contact',
optional: true,
},
},
}

View File

@@ -1197,6 +1197,18 @@ import {
import { linkedInGetProfileTool, linkedInSharePostTool } from '@/tools/linkedin'
import { linkupSearchTool } from '@/tools/linkup'
import { llmChatTool } from '@/tools/llm'
import {
loopsCreateContactPropertyTool,
loopsCreateContactTool,
loopsDeleteContactTool,
loopsFindContactTool,
loopsListContactPropertiesTool,
loopsListMailingListsTool,
loopsListTransactionalEmailsTool,
loopsSendEventTool,
loopsSendTransactionalEmailTool,
loopsUpdateContactTool,
} from '@/tools/loops'
import {
lumaAddGuestsTool,
lumaCreateEventTool,
@@ -2310,6 +2322,16 @@ export const tools: Record<string, ToolConfig> = {
jina_read_url: jinaReadUrlTool,
jina_search: jinaSearchTool,
linkup_search: linkupSearchTool,
loops_create_contact: loopsCreateContactTool,
loops_create_contact_property: loopsCreateContactPropertyTool,
loops_update_contact: loopsUpdateContactTool,
loops_find_contact: loopsFindContactTool,
loops_delete_contact: loopsDeleteContactTool,
loops_list_contact_properties: loopsListContactPropertiesTool,
loops_list_mailing_lists: loopsListMailingListsTool,
loops_list_transactional_emails: loopsListTransactionalEmailsTool,
loops_send_transactional_email: loopsSendTransactionalEmailTool,
loops_send_event: loopsSendEventTool,
luma_add_guests: lumaAddGuestsTool,
luma_create_event: lumaCreateEventTool,
luma_get_event: lumaGetEventTool,