mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
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:
@@ -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'>
|
||||
|
||||
@@ -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,
|
||||
|
||||
273
apps/docs/content/docs/en/tools/loops.mdx
Normal file
273
apps/docs/content/docs/en/tools/loops.mdx
Normal 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\) |
|
||||
|
||||
|
||||
@@ -77,6 +77,7 @@
|
||||
"linear",
|
||||
"linkedin",
|
||||
"linkup",
|
||||
"loops",
|
||||
"luma",
|
||||
"mailchimp",
|
||||
"mailgun",
|
||||
|
||||
519
apps/sim/blocks/blocks/loops.ts
Normal file
519
apps/sim/blocks/blocks/loops.ts
Normal 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)',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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'>
|
||||
|
||||
148
apps/sim/tools/loops/create_contact.ts
Normal file
148
apps/sim/tools/loops/create_contact.ts
Normal 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,
|
||||
},
|
||||
},
|
||||
}
|
||||
78
apps/sim/tools/loops/create_contact_property.ts
Normal file
78
apps/sim/tools/loops/create_contact_property.ts
Normal 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',
|
||||
},
|
||||
},
|
||||
}
|
||||
82
apps/sim/tools/loops/delete_contact.ts
Normal file
82
apps/sim/tools/loops/delete_contact.ts
Normal 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' },
|
||||
},
|
||||
}
|
||||
91
apps/sim/tools/loops/find_contact.ts
Normal file
91
apps/sim/tools/loops/find_contact.ts
Normal 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,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
10
apps/sim/tools/loops/index.ts
Normal file
10
apps/sim/tools/loops/index.ts
Normal 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'
|
||||
87
apps/sim/tools/loops/list_contact_properties.ts
Normal file
87
apps/sim/tools/loops/list_contact_properties.ts
Normal 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)',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
82
apps/sim/tools/loops/list_mailing_lists.ts
Normal file
82
apps/sim/tools/loops/list_mailing_lists.ts
Normal 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',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
135
apps/sim/tools/loops/list_transactional_emails.ts
Normal file
135
apps/sim/tools/loops/list_transactional_emails.ts
Normal 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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
112
apps/sim/tools/loops/send_event.ts
Normal file
112
apps/sim/tools/loops/send_event.ts
Normal 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' },
|
||||
},
|
||||
}
|
||||
120
apps/sim/tools/loops/send_transactional_email.ts
Normal file
120
apps/sim/tools/loops/send_transactional_email.ts
Normal 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',
|
||||
},
|
||||
},
|
||||
}
|
||||
209
apps/sim/tools/loops/types.ts
Normal file
209
apps/sim/tools/loops/types.ts
Normal 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,
|
||||
},
|
||||
}
|
||||
152
apps/sim/tools/loops/update_contact.ts
Normal file
152
apps/sim/tools/loops/update_contact.ts
Normal 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,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user