fix(loops): align integration with live API docs, add suppression + g… · simstudioai/sim@dabb856 · GitHub
Skip to content

Commit dabb856

Browse files
authored
fix(loops): align integration with live API docs, add suppression + get-template tools (#5358)
* fix(loops): align integration with live API docs, add suppression + get-template tools - fix list_transactional_emails endpoint URL (was /transactional, now /transactional-emails) - fix response fields to match actual API schema (createdAt/updatedAt, not the never-existent lastUpdated) - add loops_check_contact_suppression, loops_remove_contact_suppression, loops_get_transactional_email tools - wire new tools into block operations, outputs, and registries - alphabetize tools/registry.ts loops entries * fix(loops): expose contactId output, fix stale tool description - add missing contactId block output for check_contact_suppression (Greptile P1) - fix list_transactional_emails description to mention createdAt (Greptile P2) * fix(loops): restore lastUpdated as backwards-compat alias on list_transactional_emails Final validation pass found that /api/v1/transactional (the endpoint this tool used before this PR) is a real, functional, deprecated Loops endpoint whose schema genuinely returns lastUpdated - it was not a broken/invented field. Migrating to /api/v1/transactional-emails is still correct (current endpoint, better error semantics), but dropping lastUpdated would break any existing workflow reading it from this block's output. Keep it as a deprecated alias of updatedAt alongside the new createdAt/updatedAt fields. * fix(loops): update id output description to cover get_transactional_email
1 parent 7fd89bc commit dabb856

8 files changed

Lines changed: 482 additions & 14 deletions

File tree

apps/sim/blocks/blocks/loops.ts

Lines changed: 95 additions & 7 deletions
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import type {
2+
LoopsCheckContactSuppressionParams,
3+
LoopsCheckContactSuppressionResponse,
4+
} from '@/tools/loops/types'
5+
import type { ToolConfig } from '@/tools/types'
6+
7+
export const loopsCheckContactSuppressionTool: ToolConfig<
8+
LoopsCheckContactSuppressionParams,
9+
LoopsCheckContactSuppressionResponse
10+
> = {
11+
id: 'loops_check_contact_suppression',
12+
name: 'Loops Check Contact Suppression',
13+
description:
14+
'Check whether a Loops contact is on the suppression list (bounced, complained, or unsubscribed) by email address or userId.',
15+
version: '1.0.0',
16+
17+
params: {
18+
apiKey: {
19+
type: 'string',
20+
required: true,
21+
visibility: 'user-only',
22+
description: 'Loops API key for authentication',
23+
},
24+
email: {
25+
type: 'string',
26+
required: false,
27+
visibility: 'user-or-llm',
28+
description:
29+
'The contact email address to check (at least one of email or userId is required)',
30+
},
31+
userId: {
32+
type: 'string',
33+
required: false,
34+
visibility: 'user-or-llm',
35+
description: 'The contact userId to check (at least one of email or userId is required)',
36+
},
37+
},
38+
39+
request: {
40+
url: (params) => {
41+
if (!params.email && !params.userId) {
42+
throw new Error('At least one of email or userId is required to check suppression status')
43+
}
44+
const base = 'https://app.loops.so/api/v1/contacts/suppression'
45+
if (params.email) return `${base}?email=${encodeURIComponent(params.email.trim())}`
46+
return `${base}?userId=${encodeURIComponent(params.userId!.trim())}`
47+
},
48+
method: 'GET',
49+
headers: (params) => ({
50+
Authorization: `Bearer ${params.apiKey}`,
51+
}),
52+
},
53+
54+
transformResponse: async (response: Response) => {
55+
const data = await response.json()
56+
57+
if (data.isSuppressed == null) {
58+
return {
59+
success: false,
60+
output: {
61+
contactId: null,
62+
email: null,
63+
userId: null,
64+
isSuppressed: false,
65+
removalQuotaLimit: null,
66+
removalQuotaRemaining: null,
67+
},
68+
error: data.message ?? 'Failed to check contact suppression status',
69+
}
70+
}
71+
72+
return {
73+
success: true,
74+
output: {
75+
contactId: (data.contact?.id as string) ?? null,
76+
email: (data.contact?.email as string) ?? null,
77+
userId: (data.contact?.userId as string) ?? null,
78+
isSuppressed: (data.isSuppressed as boolean) ?? false,
79+
removalQuotaLimit: (data.removalQuota?.limit as number) ?? null,
80+
removalQuotaRemaining: (data.removalQuota?.remaining as number) ?? null,
81+
},
82+
}
83+
},
84+
85+
outputs: {
86+
contactId: { type: 'string', description: 'The Loops-assigned contact ID', optional: true },
87+
email: { type: 'string', description: 'The contact email address', optional: true },
88+
userId: { type: 'string', description: 'The contact userId', optional: true },
89+
isSuppressed: {
90+
type: 'boolean',
91+
description: 'Whether the contact is on the suppression list',
92+
},
93+
removalQuotaLimit: {
94+
type: 'number',
95+
description: 'Total suppression-removal quota for the team',
96+
optional: true,
97+
},
98+
removalQuotaRemaining: {
99+
type: 'number',
100+
description: 'Remaining suppression-removal quota for the team',
101+
optional: true,
102+
},
103+
},
104+
}
Lines changed: 106 additions & 0 deletions

0 commit comments

Comments
 (0)