mirror of
https://github.com/simstudioai/sim.git
synced 2026-03-15 03:00:33 -04:00
Compare commits
9 Commits
staging
...
waleedlati
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b2bd567b2e | ||
|
|
478bcc06fe | ||
|
|
b339b483f0 | ||
|
|
320283a35f | ||
|
|
bb8b314515 | ||
|
|
698b65f847 | ||
|
|
67fa7b628f | ||
|
|
e5fe85c44a | ||
|
|
3eca67c4ac |
@@ -4135,6 +4135,55 @@ export function LumaIcon(props: SVGProps<SVGSVGElement>) {
|
||||
)
|
||||
}
|
||||
|
||||
export function MetaAdsIcon(props: SVGProps<SVGSVGElement>) {
|
||||
const id = useId()
|
||||
const lg1 = `${id}-meta-lg1`
|
||||
const lg2 = `${id}-meta-lg2`
|
||||
return (
|
||||
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 287.56 191'>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id={lg1}
|
||||
x1='62.34'
|
||||
y1='101.45'
|
||||
x2='260.34'
|
||||
y2='91.45'
|
||||
gradientTransform='matrix(1, 0, 0, -1, 0, 192)'
|
||||
gradientUnits='userSpaceOnUse'
|
||||
>
|
||||
<stop offset='0' stopColor='#0064e1' />
|
||||
<stop offset='0.4' stopColor='#0064e1' />
|
||||
<stop offset='0.83' stopColor='#0073ee' />
|
||||
<stop offset='1' stopColor='#0082fb' />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id={lg2}
|
||||
x1='41.42'
|
||||
y1='53'
|
||||
x2='41.42'
|
||||
y2='126'
|
||||
gradientTransform='matrix(1, 0, 0, -1, 0, 192)'
|
||||
gradientUnits='userSpaceOnUse'
|
||||
>
|
||||
<stop offset='0' stopColor='#0082fb' />
|
||||
<stop offset='1' stopColor='#0064e0' />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path
|
||||
fill='#0081fb'
|
||||
d='M31.06,126c0,11,2.41,19.41,5.56,24.51A19,19,0,0,0,53.19,160c8.1,0,15.51-2,29.79-21.76,11.44-15.83,24.92-38,34-52l15.36-23.6c10.67-16.39,23-34.61,37.18-47C181.07,5.6,193.54,0,206.09,0c21.07,0,41.14,12.21,56.5,35.11,16.81,25.08,25,56.67,25,89.27,0,19.38-3.82,33.62-10.32,44.87C271,180.13,258.72,191,238.13,191V160c17.63,0,22-16.2,22-34.74,0-26.42-6.16-55.74-19.73-76.69-9.63-14.86-22.11-23.94-35.84-23.94-14.85,0-26.8,11.2-40.23,31.17-7.14,10.61-14.47,23.54-22.7,38.13l-9.06,16c-18.2,32.27-22.81,39.62-31.91,51.75C84.74,183,71.12,191,53.19,191c-21.27,0-34.72-9.21-43-23.09C3.34,156.6,0,141.76,0,124.85Z'
|
||||
/>
|
||||
<path
|
||||
fill={`url(#${lg1})`}
|
||||
d='M24.49,37.3C38.73,15.35,59.28,0,82.85,0c13.65,0,27.22,4,41.39,15.61,15.5,12.65,32,33.48,52.63,67.81l7.39,12.32c17.84,29.72,28,45,33.93,52.22,7.64,9.26,13,12,19.94,12,17.63,0,22-16.2,22-34.74l27.4-.86c0,19.38-3.82,33.62-10.32,44.87C271,180.13,258.72,191,238.13,191c-12.8,0-24.14-2.78-36.68-14.61-9.64-9.08-20.91-25.21-29.58-39.71L146.08,93.6c-12.94-21.62-24.81-37.74-31.68-45C107,40.71,97.51,31.23,82.35,31.23c-12.27,0-22.69,8.61-31.41,21.78Z'
|
||||
/>
|
||||
<path
|
||||
fill={`url(#${lg2})`}
|
||||
d='M82.35,31.23c-12.27,0-22.69,8.61-31.41,21.78C38.61,71.62,31.06,99.34,31.06,126c0,11,2.41,19.41,5.56,24.51L10.14,167.91C3.34,156.6,0,141.76,0,124.85,0,94.1,8.44,62.05,24.49,37.3,38.73,15.35,59.28,0,82.85,0Z'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
export function MailchimpIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
|
||||
@@ -95,6 +95,7 @@ import {
|
||||
MailgunIcon,
|
||||
MailServerIcon,
|
||||
Mem0Icon,
|
||||
MetaAdsIcon,
|
||||
MicrosoftDataverseIcon,
|
||||
MicrosoftExcelIcon,
|
||||
MicrosoftOneDriveIcon,
|
||||
@@ -263,6 +264,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
mailgun: MailgunIcon,
|
||||
mem0: Mem0Icon,
|
||||
memory: BrainIcon,
|
||||
meta_ads: MetaAdsIcon,
|
||||
microsoft_dataverse: MicrosoftDataverseIcon,
|
||||
microsoft_excel_v2: MicrosoftExcelIcon,
|
||||
microsoft_planner: MicrosoftPlannerIcon,
|
||||
|
||||
@@ -138,6 +138,26 @@ Get the full transcript of a recording
|
||||
| ↳ `end` | number | End timestamp in ms |
|
||||
| ↳ `text` | string | Transcript text |
|
||||
|
||||
### `grain_list_views`
|
||||
|
||||
List available Grain views for webhook subscriptions
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Grain API key \(Personal Access Token\) |
|
||||
| `typeFilter` | string | No | Optional view type filter: recordings, highlights, or stories |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `views` | array | Array of Grain views |
|
||||
| ↳ `id` | string | View UUID |
|
||||
| ↳ `name` | string | View name |
|
||||
| ↳ `type` | string | View type: recordings, highlights, or stories |
|
||||
|
||||
### `grain_list_teams`
|
||||
|
||||
List all teams in the workspace
|
||||
@@ -185,15 +205,9 @@ Create a webhook to receive recording events
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Grain API key \(Personal Access Token\) |
|
||||
| `hookUrl` | string | Yes | Webhook endpoint URL \(e.g., "https://example.com/webhooks/grain"\) |
|
||||
| `hookType` | string | Yes | Type of webhook: "recording_added" or "upload_status" |
|
||||
| `filterBeforeDatetime` | string | No | Filter: recordings before this ISO8601 date \(e.g., "2024-01-15T00:00:00Z"\) |
|
||||
| `filterAfterDatetime` | string | No | Filter: recordings after this ISO8601 date \(e.g., "2024-01-01T00:00:00Z"\) |
|
||||
| `filterParticipantScope` | string | No | Filter: "internal" or "external" |
|
||||
| `filterTeamId` | string | No | Filter: specific team UUID \(e.g., "a1b2c3d4-e5f6-7890-abcd-ef1234567890"\) |
|
||||
| `filterMeetingTypeId` | string | No | Filter: specific meeting type UUID \(e.g., "a1b2c3d4-e5f6-7890-abcd-ef1234567890"\) |
|
||||
| `includeHighlights` | boolean | No | Include highlights in webhook payload |
|
||||
| `includeParticipants` | boolean | No | Include participants in webhook payload |
|
||||
| `includeAiSummary` | boolean | No | Include AI summary in webhook payload |
|
||||
| `viewId` | string | Yes | Grain view ID from GET /_/public-api/views |
|
||||
| `actions` | array | No | Optional list of actions to subscribe to: added, updated, removed |
|
||||
| `items` | string | No | No description |
|
||||
|
||||
#### Output
|
||||
|
||||
@@ -202,9 +216,8 @@ Create a webhook to receive recording events
|
||||
| `id` | string | Hook UUID |
|
||||
| `enabled` | boolean | Whether hook is active |
|
||||
| `hook_url` | string | The webhook URL |
|
||||
| `hook_type` | string | Type of hook: recording_added or upload_status |
|
||||
| `filter` | object | Applied filters |
|
||||
| `include` | object | Included fields |
|
||||
| `view_id` | string | Grain view ID for the webhook |
|
||||
| `actions` | array | Configured actions for the webhook |
|
||||
| `inserted_at` | string | ISO8601 creation timestamp |
|
||||
|
||||
### `grain_list_hooks`
|
||||
@@ -225,9 +238,8 @@ List all webhooks for the account
|
||||
| ↳ `id` | string | Hook UUID |
|
||||
| ↳ `enabled` | boolean | Whether hook is active |
|
||||
| ↳ `hook_url` | string | Webhook URL |
|
||||
| ↳ `hook_type` | string | Type: recording_added or upload_status |
|
||||
| ↳ `filter` | object | Applied filters |
|
||||
| ↳ `include` | object | Included fields |
|
||||
| ↳ `view_id` | string | Grain view ID |
|
||||
| ↳ `actions` | array | Configured actions |
|
||||
| ↳ `inserted_at` | string | Creation timestamp |
|
||||
|
||||
### `grain_delete_hook`
|
||||
|
||||
@@ -92,6 +92,7 @@
|
||||
"mailgun",
|
||||
"mem0",
|
||||
"memory",
|
||||
"meta_ads",
|
||||
"microsoft_dataverse",
|
||||
"microsoft_excel",
|
||||
"microsoft_planner",
|
||||
|
||||
176
apps/docs/content/docs/en/tools/meta_ads.mdx
Normal file
176
apps/docs/content/docs/en/tools/meta_ads.mdx
Normal file
@@ -0,0 +1,176 @@
|
||||
---
|
||||
title: Meta Ads
|
||||
description: Query campaigns, ad sets, ads, and performance insights
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="meta_ads"
|
||||
color="#1877F2"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
[Meta Ads](https://www.facebook.com/business/ads) is Meta's advertising platform that lets businesses create and manage ads across Facebook, Instagram, Messenger, and the Audience Network. It offers campaign management at multiple levels — campaigns, ad sets, and individual ads — with detailed targeting, budgeting, and performance analytics.
|
||||
|
||||
In Sim, the Meta Ads integration enables your agents to query account information, browse campaign hierarchies, and pull performance insights including impressions, clicks, spend, CTR, CPC, CPM, reach, frequency, and conversions. This supports use cases such as automated performance reporting, spend monitoring, campaign auditing, and cross-channel analytics workflows. Account and campaign selectors provide a dropdown experience so agents and users can pick resources without manually entering IDs.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Connect to Meta Ads to view account info, list campaigns, ad sets, and ads, and get performance insights and metrics.
|
||||
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
### `meta_ads_get_account`
|
||||
|
||||
Get information about a Meta Ads account
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accountId` | string | Yes | Meta Ads account ID \(numeric, without act_ prefix\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | Ad account ID |
|
||||
| `name` | string | Ad account name |
|
||||
| `accountStatus` | number | Account status code |
|
||||
| `currency` | string | Account currency \(e.g., USD\) |
|
||||
| `timezone` | string | Account timezone |
|
||||
| `amountSpent` | string | Total amount spent |
|
||||
| `spendCap` | string | Spending limit for the account |
|
||||
| `businessCountryCode` | string | Country code for the business |
|
||||
|
||||
### `meta_ads_list_campaigns`
|
||||
|
||||
List campaigns in a Meta Ads account with optional status filtering
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accountId` | string | Yes | Meta Ads account ID \(numeric, without act_ prefix\) |
|
||||
| `status` | string | No | Filter by campaign status \(ACTIVE, PAUSED, ARCHIVED, DELETED\) |
|
||||
| `limit` | number | No | Maximum number of campaigns to return |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `campaigns` | array | List of campaigns in the account |
|
||||
| ↳ `id` | string | Campaign ID |
|
||||
| ↳ `name` | string | Campaign name |
|
||||
| ↳ `status` | string | Campaign status \(ACTIVE, PAUSED, etc.\) |
|
||||
| ↳ `objective` | string | Campaign objective |
|
||||
| ↳ `dailyBudget` | string | Daily budget in account currency cents |
|
||||
| ↳ `lifetimeBudget` | string | Lifetime budget in account currency cents |
|
||||
| ↳ `createdTime` | string | Campaign creation time \(ISO 8601\) |
|
||||
| ↳ `updatedTime` | string | Campaign last update time \(ISO 8601\) |
|
||||
| `totalCount` | number | Number of campaigns returned in this response \(may be limited by pagination\) |
|
||||
|
||||
### `meta_ads_list_ad_sets`
|
||||
|
||||
List ad sets in a Meta Ads account or campaign
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accountId` | string | Yes | Meta Ads account ID \(numeric, without act_ prefix\) |
|
||||
| `campaignId` | string | No | Filter ad sets by campaign ID |
|
||||
| `status` | string | No | Filter by ad set status \(ACTIVE, PAUSED, ARCHIVED, DELETED\) |
|
||||
| `limit` | number | No | Maximum number of ad sets to return |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `adSets` | array | List of ad sets |
|
||||
| ↳ `id` | string | Ad set ID |
|
||||
| ↳ `name` | string | Ad set name |
|
||||
| ↳ `status` | string | Ad set status |
|
||||
| ↳ `campaignId` | string | Parent campaign ID |
|
||||
| ↳ `dailyBudget` | string | Daily budget in account currency cents |
|
||||
| ↳ `lifetimeBudget` | string | Lifetime budget in account currency cents |
|
||||
| ↳ `startTime` | string | Ad set start time \(ISO 8601\) |
|
||||
| ↳ `endTime` | string | Ad set end time \(ISO 8601\) |
|
||||
| `totalCount` | number | Number of ad sets returned in this response \(may be limited by pagination\) |
|
||||
|
||||
### `meta_ads_list_ads`
|
||||
|
||||
List ads in a Meta Ads account, campaign, or ad set
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accountId` | string | Yes | Meta Ads account ID \(numeric, without act_ prefix\) |
|
||||
| `campaignId` | string | No | Filter ads by campaign ID |
|
||||
| `adSetId` | string | No | Filter ads by ad set ID |
|
||||
| `status` | string | No | Filter by ad status \(ACTIVE, PAUSED, ARCHIVED, DELETED\) |
|
||||
| `limit` | number | No | Maximum number of ads to return |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ads` | array | List of ads |
|
||||
| ↳ `id` | string | Ad ID |
|
||||
| ↳ `name` | string | Ad name |
|
||||
| ↳ `status` | string | Ad status |
|
||||
| ↳ `adSetId` | string | Parent ad set ID |
|
||||
| ↳ `campaignId` | string | Parent campaign ID |
|
||||
| ↳ `createdTime` | string | Ad creation time \(ISO 8601\) |
|
||||
| ↳ `updatedTime` | string | Ad last update time \(ISO 8601\) |
|
||||
| `totalCount` | number | Number of ads returned in this response \(may be limited by pagination\) |
|
||||
|
||||
### `meta_ads_get_insights`
|
||||
|
||||
Get performance insights and metrics for Meta Ads campaigns, ad sets, or ads
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accountId` | string | Yes | Meta Ads account ID \(numeric, without act_ prefix\) |
|
||||
| `level` | string | Yes | Aggregation level for insights \(account, campaign, adset, ad\) |
|
||||
| `campaignId` | string | No | Filter insights by campaign ID |
|
||||
| `adSetId` | string | No | Filter insights by ad set ID |
|
||||
| `datePreset` | string | No | Predefined date range \(today, yesterday, last_7d, last_14d, last_28d, last_30d, last_90d, maximum, this_month, last_month\) |
|
||||
| `startDate` | string | No | Custom start date in YYYY-MM-DD format |
|
||||
| `endDate` | string | No | Custom end date in YYYY-MM-DD format |
|
||||
| `limit` | number | No | Maximum number of insight rows to return |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `insights` | array | Performance insight rows |
|
||||
| ↳ `accountId` | string | Ad account ID |
|
||||
| ↳ `campaignId` | string | Campaign ID |
|
||||
| ↳ `campaignName` | string | Campaign name |
|
||||
| ↳ `adSetId` | string | Ad set ID |
|
||||
| ↳ `adSetName` | string | Ad set name |
|
||||
| ↳ `adId` | string | Ad ID |
|
||||
| ↳ `adName` | string | Ad name |
|
||||
| ↳ `impressions` | string | Number of impressions |
|
||||
| ↳ `clicks` | string | Number of clicks |
|
||||
| ↳ `spend` | string | Amount spent in account currency |
|
||||
| ↳ `ctr` | string | Click-through rate |
|
||||
| ↳ `cpc` | string | Cost per click |
|
||||
| ↳ `cpm` | string | Cost per 1,000 impressions |
|
||||
| ↳ `reach` | string | Number of unique users reached |
|
||||
| ↳ `frequency` | string | Average number of times each person saw the ad |
|
||||
| ↳ `conversions` | number | Total conversions from actions |
|
||||
| ↳ `dateStart` | string | Start date of the reporting period |
|
||||
| ↳ `dateStop` | string | End date of the reporting period |
|
||||
| `totalCount` | number | Number of insight rows returned in this response \(may be limited by pagination\) |
|
||||
|
||||
|
||||
105
apps/sim/app/api/tools/meta_ads/accounts/route.ts
Normal file
105
apps/sim/app/api/tools/meta_ads/accounts/route.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
||||
import { getMetaApiBaseUrl } from '@/tools/meta_ads/types'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('MetaAdsAccountsAPI')
|
||||
|
||||
interface MetaAdAccount {
|
||||
id: string
|
||||
account_id: string
|
||||
name: string
|
||||
account_status: number
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const requestId = generateRequestId()
|
||||
const body = await request.json()
|
||||
const { credential, workflowId } = body
|
||||
|
||||
if (!credential) {
|
||||
logger.error('Missing credential in request')
|
||||
return NextResponse.json({ error: 'Credential is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const authz = await authorizeCredentialUse(request as any, {
|
||||
credentialId: credential,
|
||||
workflowId,
|
||||
})
|
||||
if (!authz.ok || !authz.credentialOwnerUserId) {
|
||||
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
|
||||
}
|
||||
|
||||
const accessToken = await refreshAccessTokenIfNeeded(
|
||||
credential,
|
||||
authz.credentialOwnerUserId,
|
||||
requestId
|
||||
)
|
||||
if (!accessToken) {
|
||||
logger.error('Failed to get access token', {
|
||||
credentialId: credential,
|
||||
userId: authz.credentialOwnerUserId,
|
||||
})
|
||||
return NextResponse.json(
|
||||
{ error: 'Could not retrieve access token', authRequired: true },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
const url = `${getMetaApiBaseUrl()}/me/adaccounts?fields=account_id,name,account_status&limit=200`
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json()
|
||||
const errorMessage = errorData?.error?.message ?? 'Failed to fetch ad accounts'
|
||||
logger.error('Meta API error', { status: response.status, error: errorMessage })
|
||||
return NextResponse.json({ error: errorMessage }, { status: response.status })
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
const items: MetaAdAccount[] = data.data ?? []
|
||||
|
||||
const META_ACCOUNT_STATUS: Record<number, string> = {
|
||||
1: 'Active',
|
||||
2: 'Disabled',
|
||||
3: 'Unsettled',
|
||||
7: 'Pending Risk Review',
|
||||
8: 'Pending Settlement',
|
||||
9: 'In Grace Period',
|
||||
100: 'Pending Closure',
|
||||
101: 'Closed',
|
||||
201: 'Any Active',
|
||||
202: 'Any Closed',
|
||||
}
|
||||
|
||||
const accounts = items.map((account) => {
|
||||
const statusLabel = META_ACCOUNT_STATUS[account.account_status] ?? 'Unknown'
|
||||
const suffix = account.account_status !== 1 ? ` (${statusLabel})` : ''
|
||||
return {
|
||||
id: account.account_id,
|
||||
name: `${account.name || `Account ${account.account_id}`}${suffix}`,
|
||||
}
|
||||
})
|
||||
|
||||
logger.info(`Successfully fetched ${accounts.length} Meta ad accounts`, {
|
||||
total: items.length,
|
||||
active: accounts.length,
|
||||
})
|
||||
|
||||
return NextResponse.json({ accounts })
|
||||
} catch (error) {
|
||||
logger.error('Error processing Meta ad accounts request:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to retrieve ad accounts', details: (error as Error).message },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
94
apps/sim/app/api/tools/meta_ads/campaigns/route.ts
Normal file
94
apps/sim/app/api/tools/meta_ads/campaigns/route.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
||||
import { getMetaApiBaseUrl, stripActPrefix } from '@/tools/meta_ads/types'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('MetaAdsCampaignsAPI')
|
||||
|
||||
interface MetaCampaign {
|
||||
id: string
|
||||
name: string
|
||||
status: string
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const requestId = generateRequestId()
|
||||
const body = await request.json()
|
||||
const { credential, workflowId, accountId } = body
|
||||
|
||||
if (!credential) {
|
||||
logger.error('Missing credential in request')
|
||||
return NextResponse.json({ error: 'Credential is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!accountId) {
|
||||
logger.error('Missing accountId in request')
|
||||
return NextResponse.json({ error: 'Account ID is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const authz = await authorizeCredentialUse(request as any, {
|
||||
credentialId: credential,
|
||||
workflowId,
|
||||
})
|
||||
if (!authz.ok || !authz.credentialOwnerUserId) {
|
||||
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
|
||||
}
|
||||
|
||||
const accessToken = await refreshAccessTokenIfNeeded(
|
||||
credential,
|
||||
authz.credentialOwnerUserId,
|
||||
requestId
|
||||
)
|
||||
if (!accessToken) {
|
||||
logger.error('Failed to get access token', {
|
||||
credentialId: credential,
|
||||
userId: authz.credentialOwnerUserId,
|
||||
})
|
||||
return NextResponse.json(
|
||||
{ error: 'Could not retrieve access token', authRequired: true },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
const trimmedId = stripActPrefix(String(accountId))
|
||||
const url = `${getMetaApiBaseUrl()}/act_${trimmedId}/campaigns?fields=id,name,status&limit=200`
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json()
|
||||
const errorMessage = errorData?.error?.message ?? 'Failed to fetch campaigns'
|
||||
logger.error('Meta API error', { status: response.status, error: errorMessage })
|
||||
return NextResponse.json({ error: errorMessage }, { status: response.status })
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
const items: MetaCampaign[] = data.data ?? []
|
||||
|
||||
const campaigns = items.map((campaign) => ({
|
||||
id: campaign.id,
|
||||
name: campaign.name || `Campaign ${campaign.id}`,
|
||||
status: campaign.status,
|
||||
}))
|
||||
|
||||
logger.info(`Successfully fetched ${campaigns.length} Meta campaigns`, {
|
||||
accountId: trimmedId,
|
||||
total: campaigns.length,
|
||||
})
|
||||
|
||||
return NextResponse.json({ campaigns })
|
||||
} catch (error) {
|
||||
logger.error('Error processing Meta campaigns request:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to retrieve campaigns', details: (error as Error).message },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
268
apps/sim/blocks/blocks/meta_ads.ts
Normal file
268
apps/sim/blocks/blocks/meta_ads.ts
Normal file
@@ -0,0 +1,268 @@
|
||||
import { MetaAdsIcon } from '@/components/icons'
|
||||
import { getScopesForService } from '@/lib/oauth/utils'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
|
||||
export const MetaAdsBlock: BlockConfig = {
|
||||
type: 'meta_ads',
|
||||
name: 'Meta Ads',
|
||||
description: 'Query campaigns, ad sets, ads, and performance insights',
|
||||
longDescription:
|
||||
'Connect to Meta Ads to view account info, list campaigns, ad sets, and ads, and get performance insights and metrics.',
|
||||
docsLink: 'https://docs.sim.ai/tools/meta_ads',
|
||||
category: 'tools',
|
||||
bgColor: '#1877F2',
|
||||
icon: MetaAdsIcon,
|
||||
authMode: AuthMode.OAuth,
|
||||
subBlocks: [
|
||||
{
|
||||
id: 'operation',
|
||||
title: 'Operation',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'Get Account Info', id: 'get_account' },
|
||||
{ label: 'List Campaigns', id: 'list_campaigns' },
|
||||
{ label: 'List Ad Sets', id: 'list_ad_sets' },
|
||||
{ label: 'List Ads', id: 'list_ads' },
|
||||
{ label: 'Get Insights', id: 'get_insights' },
|
||||
],
|
||||
value: () => 'list_campaigns',
|
||||
},
|
||||
|
||||
{
|
||||
id: 'credential',
|
||||
title: 'Meta Ads Account',
|
||||
type: 'oauth-input',
|
||||
canonicalParamId: 'oauthCredential',
|
||||
mode: 'basic',
|
||||
required: true,
|
||||
serviceId: 'meta-ads',
|
||||
requiredScopes: getScopesForService('meta-ads'),
|
||||
placeholder: 'Select Meta Ads account',
|
||||
},
|
||||
{
|
||||
id: 'manualCredential',
|
||||
title: 'Meta Ads Account',
|
||||
type: 'short-input',
|
||||
canonicalParamId: 'oauthCredential',
|
||||
mode: 'advanced',
|
||||
placeholder: 'Enter credential ID',
|
||||
required: true,
|
||||
},
|
||||
|
||||
{
|
||||
id: 'accountSelector',
|
||||
title: 'Ad Account',
|
||||
type: 'project-selector',
|
||||
canonicalParamId: 'accountId',
|
||||
serviceId: 'meta-ads',
|
||||
selectorKey: 'meta-ads.accounts',
|
||||
placeholder: 'Select ad account',
|
||||
dependsOn: { any: ['credential', 'manualCredential'] },
|
||||
mode: 'basic',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'manualAccountId',
|
||||
title: 'Account ID',
|
||||
type: 'short-input',
|
||||
canonicalParamId: 'accountId',
|
||||
placeholder: 'Meta Ads account ID (numeric, without act_ prefix)',
|
||||
mode: 'advanced',
|
||||
required: true,
|
||||
},
|
||||
|
||||
{
|
||||
id: 'campaignSelector',
|
||||
title: 'Campaign',
|
||||
type: 'project-selector',
|
||||
canonicalParamId: 'campaignId',
|
||||
serviceId: 'meta-ads',
|
||||
selectorKey: 'meta-ads.campaigns',
|
||||
placeholder: 'Select campaign',
|
||||
dependsOn: { all: ['credential'], any: ['accountSelector', 'manualAccountId'] },
|
||||
mode: 'basic',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['list_ad_sets', 'list_ads', 'get_insights'],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'manualCampaignId',
|
||||
title: 'Campaign ID',
|
||||
type: 'short-input',
|
||||
canonicalParamId: 'campaignId',
|
||||
placeholder: 'Campaign ID to filter by',
|
||||
mode: 'advanced',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['list_ad_sets', 'list_ads', 'get_insights'],
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
id: 'adSetId',
|
||||
title: 'Ad Set ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'Ad set ID to filter by',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['list_ads', 'get_insights'],
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
id: 'level',
|
||||
title: 'Insights Level',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'Account', id: 'account' },
|
||||
{ label: 'Campaign', id: 'campaign' },
|
||||
{ label: 'Ad Set', id: 'adset' },
|
||||
{ label: 'Ad', id: 'ad' },
|
||||
],
|
||||
condition: { field: 'operation', value: 'get_insights' },
|
||||
required: { field: 'operation', value: 'get_insights' },
|
||||
value: () => 'campaign',
|
||||
},
|
||||
|
||||
{
|
||||
id: 'datePreset',
|
||||
title: 'Date Range',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'Last 30 Days', id: 'last_30d' },
|
||||
{ label: 'Last 7 Days', id: 'last_7d' },
|
||||
{ label: 'Last 14 Days', id: 'last_14d' },
|
||||
{ label: 'Last 28 Days', id: 'last_28d' },
|
||||
{ label: 'Last 90 Days', id: 'last_90d' },
|
||||
{ label: 'Maximum', id: 'maximum' },
|
||||
{ label: 'Today', id: 'today' },
|
||||
{ label: 'Yesterday', id: 'yesterday' },
|
||||
{ label: 'This Month', id: 'this_month' },
|
||||
{ label: 'Last Month', id: 'last_month' },
|
||||
{ label: 'Custom', id: 'CUSTOM' },
|
||||
],
|
||||
condition: { field: 'operation', value: 'get_insights' },
|
||||
value: () => 'last_30d',
|
||||
},
|
||||
|
||||
{
|
||||
id: 'startDate',
|
||||
title: 'Start Date',
|
||||
type: 'short-input',
|
||||
placeholder: 'YYYY-MM-DD',
|
||||
condition: { field: 'datePreset', value: 'CUSTOM' },
|
||||
required: { field: 'datePreset', value: 'CUSTOM' },
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
prompt:
|
||||
'Generate a date in YYYY-MM-DD format. Return ONLY the date string - no explanations, no extra text.',
|
||||
generationType: 'timestamp',
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
id: 'endDate',
|
||||
title: 'End Date',
|
||||
type: 'short-input',
|
||||
placeholder: 'YYYY-MM-DD',
|
||||
condition: { field: 'datePreset', value: 'CUSTOM' },
|
||||
required: { field: 'datePreset', value: 'CUSTOM' },
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
prompt:
|
||||
'Generate a date in YYYY-MM-DD format. Return ONLY the date string - no explanations, no extra text.',
|
||||
generationType: 'timestamp',
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
id: 'status',
|
||||
title: 'Status Filter',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'All', id: '' },
|
||||
{ label: 'Active', id: 'ACTIVE' },
|
||||
{ label: 'Paused', id: 'PAUSED' },
|
||||
{ label: 'Archived', id: 'ARCHIVED' },
|
||||
{ label: 'Deleted', id: 'DELETED' },
|
||||
],
|
||||
mode: 'advanced',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['list_campaigns', 'list_ad_sets', 'list_ads'],
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
id: 'limit',
|
||||
title: 'Limit',
|
||||
type: 'short-input',
|
||||
placeholder: 'Maximum results to return',
|
||||
mode: 'advanced',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['list_campaigns', 'list_ad_sets', 'list_ads', 'get_insights'],
|
||||
},
|
||||
},
|
||||
],
|
||||
tools: {
|
||||
access: [
|
||||
'meta_ads_get_account',
|
||||
'meta_ads_list_campaigns',
|
||||
'meta_ads_list_ad_sets',
|
||||
'meta_ads_list_ads',
|
||||
'meta_ads_get_insights',
|
||||
],
|
||||
config: {
|
||||
tool: (params) => `meta_ads_${params.operation}`,
|
||||
params: (params) => {
|
||||
const { oauthCredential, datePreset, limit, ...rest } = params
|
||||
|
||||
const result: Record<string, unknown> = {
|
||||
...rest,
|
||||
oauthCredential,
|
||||
}
|
||||
|
||||
if (datePreset && datePreset !== 'CUSTOM') {
|
||||
result.datePreset = datePreset
|
||||
}
|
||||
|
||||
if (limit !== undefined && limit !== '') {
|
||||
result.limit = Number(limit)
|
||||
}
|
||||
|
||||
return result
|
||||
},
|
||||
},
|
||||
},
|
||||
inputs: {
|
||||
operation: { type: 'string', description: 'Operation to perform' },
|
||||
oauthCredential: { type: 'string', description: 'Meta Ads OAuth credential' },
|
||||
accountId: { type: 'string', description: 'Meta Ads account ID' },
|
||||
campaignId: { type: 'string', description: 'Campaign ID to filter by' },
|
||||
adSetId: { type: 'string', description: 'Ad set ID to filter by' },
|
||||
level: { type: 'string', description: 'Insights aggregation level' },
|
||||
datePreset: { type: 'string', description: 'Date range for insights' },
|
||||
startDate: { type: 'string', description: 'Custom start date (YYYY-MM-DD)' },
|
||||
endDate: { type: 'string', description: 'Custom end date (YYYY-MM-DD)' },
|
||||
status: { type: 'string', description: 'Status filter' },
|
||||
limit: { type: 'number', description: 'Maximum results to return' },
|
||||
},
|
||||
outputs: {
|
||||
id: { type: 'string', description: 'Account ID (get_account)' },
|
||||
name: { type: 'string', description: 'Account name (get_account)' },
|
||||
accountStatus: { type: 'number', description: 'Account status code (get_account)' },
|
||||
currency: { type: 'string', description: 'Account currency (get_account)' },
|
||||
timezone: { type: 'string', description: 'Account timezone (get_account)' },
|
||||
amountSpent: { type: 'string', description: 'Total amount spent (get_account)' },
|
||||
spendCap: { type: 'string', description: 'Spending limit (get_account)' },
|
||||
businessCountryCode: { type: 'string', description: 'Country code (get_account)' },
|
||||
campaigns: { type: 'json', description: 'Campaign data (list_campaigns)' },
|
||||
adSets: { type: 'json', description: 'Ad set data (list_ad_sets)' },
|
||||
ads: { type: 'json', description: 'Ad data (list_ads)' },
|
||||
insights: { type: 'json', description: 'Performance insights (get_insights)' },
|
||||
totalCount: { type: 'number', description: 'Total number of results' },
|
||||
},
|
||||
}
|
||||
@@ -102,6 +102,7 @@ import { ManualTriggerBlock } from '@/blocks/blocks/manual_trigger'
|
||||
import { McpBlock } from '@/blocks/blocks/mcp'
|
||||
import { Mem0Block } from '@/blocks/blocks/mem0'
|
||||
import { MemoryBlock } from '@/blocks/blocks/memory'
|
||||
import { MetaAdsBlock } from '@/blocks/blocks/meta_ads'
|
||||
import { MicrosoftDataverseBlock } from '@/blocks/blocks/microsoft_dataverse'
|
||||
import { MicrosoftExcelBlock, MicrosoftExcelV2Block } from '@/blocks/blocks/microsoft_excel'
|
||||
import { MicrosoftPlannerBlock } from '@/blocks/blocks/microsoft_planner'
|
||||
@@ -315,6 +316,7 @@ export const registry: Record<string, BlockConfig> = {
|
||||
mcp: McpBlock,
|
||||
mem0: Mem0Block,
|
||||
memory: MemoryBlock,
|
||||
meta_ads: MetaAdsBlock,
|
||||
microsoft_dataverse: MicrosoftDataverseBlock,
|
||||
microsoft_excel: MicrosoftExcelBlock,
|
||||
microsoft_excel_v2: MicrosoftExcelV2Block,
|
||||
|
||||
@@ -4135,6 +4135,55 @@ export function LumaIcon(props: SVGProps<SVGSVGElement>) {
|
||||
)
|
||||
}
|
||||
|
||||
export function MetaAdsIcon(props: SVGProps<SVGSVGElement>) {
|
||||
const id = useId()
|
||||
const lg1 = `${id}-meta-lg1`
|
||||
const lg2 = `${id}-meta-lg2`
|
||||
return (
|
||||
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 287.56 191'>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id={lg1}
|
||||
x1='62.34'
|
||||
y1='101.45'
|
||||
x2='260.34'
|
||||
y2='91.45'
|
||||
gradientTransform='matrix(1, 0, 0, -1, 0, 192)'
|
||||
gradientUnits='userSpaceOnUse'
|
||||
>
|
||||
<stop offset='0' stopColor='#0064e1' />
|
||||
<stop offset='0.4' stopColor='#0064e1' />
|
||||
<stop offset='0.83' stopColor='#0073ee' />
|
||||
<stop offset='1' stopColor='#0082fb' />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id={lg2}
|
||||
x1='41.42'
|
||||
y1='53'
|
||||
x2='41.42'
|
||||
y2='126'
|
||||
gradientTransform='matrix(1, 0, 0, -1, 0, 192)'
|
||||
gradientUnits='userSpaceOnUse'
|
||||
>
|
||||
<stop offset='0' stopColor='#0082fb' />
|
||||
<stop offset='1' stopColor='#0064e0' />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path
|
||||
fill='#0081fb'
|
||||
d='M31.06,126c0,11,2.41,19.41,5.56,24.51A19,19,0,0,0,53.19,160c8.1,0,15.51-2,29.79-21.76,11.44-15.83,24.92-38,34-52l15.36-23.6c10.67-16.39,23-34.61,37.18-47C181.07,5.6,193.54,0,206.09,0c21.07,0,41.14,12.21,56.5,35.11,16.81,25.08,25,56.67,25,89.27,0,19.38-3.82,33.62-10.32,44.87C271,180.13,258.72,191,238.13,191V160c17.63,0,22-16.2,22-34.74,0-26.42-6.16-55.74-19.73-76.69-9.63-14.86-22.11-23.94-35.84-23.94-14.85,0-26.8,11.2-40.23,31.17-7.14,10.61-14.47,23.54-22.7,38.13l-9.06,16c-18.2,32.27-22.81,39.62-31.91,51.75C84.74,183,71.12,191,53.19,191c-21.27,0-34.72-9.21-43-23.09C3.34,156.6,0,141.76,0,124.85Z'
|
||||
/>
|
||||
<path
|
||||
fill={`url(#${lg1})`}
|
||||
d='M24.49,37.3C38.73,15.35,59.28,0,82.85,0c13.65,0,27.22,4,41.39,15.61,15.5,12.65,32,33.48,52.63,67.81l7.39,12.32c17.84,29.72,28,45,33.93,52.22,7.64,9.26,13,12,19.94,12,17.63,0,22-16.2,22-34.74l27.4-.86c0,19.38-3.82,33.62-10.32,44.87C271,180.13,258.72,191,238.13,191c-12.8,0-24.14-2.78-36.68-14.61-9.64-9.08-20.91-25.21-29.58-39.71L146.08,93.6c-12.94-21.62-24.81-37.74-31.68-45C107,40.71,97.51,31.23,82.35,31.23c-12.27,0-22.69,8.61-31.41,21.78Z'
|
||||
/>
|
||||
<path
|
||||
fill={`url(#${lg2})`}
|
||||
d='M82.35,31.23c-12.27,0-22.69,8.61-31.41,21.78C38.61,71.62,31.06,99.34,31.06,126c0,11,2.41,19.41,5.56,24.51L10.14,167.91C3.34,156.6,0,141.76,0,124.85,0,94.1,8.44,62.05,24.49,37.3,38.73,15.35,59.28,0,82.85,0Z'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
export function MailchimpIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
|
||||
@@ -1254,6 +1254,54 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
}))
|
||||
},
|
||||
},
|
||||
'meta-ads.accounts': {
|
||||
key: 'meta-ads.accounts',
|
||||
staleTime: SELECTOR_STALE,
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'meta-ads.accounts',
|
||||
context.oauthCredential ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'meta-ads.accounts')
|
||||
const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId })
|
||||
const data = await fetchJson<{ accounts: { id: string; name: string }[] }>(
|
||||
'/api/tools/meta_ads/accounts',
|
||||
{ method: 'POST', body }
|
||||
)
|
||||
return (data.accounts || []).map((account) => ({
|
||||
id: account.id,
|
||||
label: account.name,
|
||||
}))
|
||||
},
|
||||
},
|
||||
'meta-ads.campaigns': {
|
||||
key: 'meta-ads.campaigns',
|
||||
staleTime: SELECTOR_STALE,
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'meta-ads.campaigns',
|
||||
context.oauthCredential ?? 'none',
|
||||
context.accountId ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential && context.accountId),
|
||||
fetchList: async ({ context }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'meta-ads.campaigns')
|
||||
const body = JSON.stringify({
|
||||
credential: credentialId,
|
||||
workflowId: context.workflowId,
|
||||
accountId: context.accountId,
|
||||
})
|
||||
const data = await fetchJson<{
|
||||
campaigns: { id: string; name: string; status: string }[]
|
||||
}>('/api/tools/meta_ads/campaigns', { method: 'POST', body })
|
||||
return (data.campaigns || []).map((campaign) => ({
|
||||
id: campaign.id,
|
||||
label: `${campaign.name} (${campaign.status})`,
|
||||
}))
|
||||
},
|
||||
},
|
||||
'linear.projects': {
|
||||
key: 'linear.projects',
|
||||
staleTime: SELECTOR_STALE,
|
||||
|
||||
@@ -31,6 +31,8 @@ export type SelectorKey =
|
||||
| 'jira.projects'
|
||||
| 'linear.projects'
|
||||
| 'linear.teams'
|
||||
| 'meta-ads.accounts'
|
||||
| 'meta-ads.campaigns'
|
||||
| 'confluence.pages'
|
||||
| 'microsoft.teams'
|
||||
| 'microsoft.chats'
|
||||
@@ -77,6 +79,7 @@ export interface SelectorContext {
|
||||
baseId?: string
|
||||
datasetId?: string
|
||||
serviceDeskId?: string
|
||||
accountId?: string
|
||||
}
|
||||
|
||||
export interface SelectorQueryArgs {
|
||||
|
||||
@@ -323,6 +323,39 @@ export const auth = betterAuth({
|
||||
}
|
||||
}
|
||||
|
||||
if (account.providerId === 'meta-ads' && account.accessToken) {
|
||||
try {
|
||||
const exchangeUrl = new URL('https://graph.facebook.com/v24.0/oauth/access_token')
|
||||
exchangeUrl.searchParams.set('grant_type', 'fb_exchange_token')
|
||||
exchangeUrl.searchParams.set('client_id', env.META_ADS_CLIENT_ID as string)
|
||||
exchangeUrl.searchParams.set('client_secret', env.META_ADS_CLIENT_SECRET as string)
|
||||
exchangeUrl.searchParams.set('fb_exchange_token', account.accessToken)
|
||||
|
||||
const exchangeResponse = await fetch(exchangeUrl.toString())
|
||||
if (exchangeResponse.ok) {
|
||||
const exchangeData = await exchangeResponse.json()
|
||||
const longLivedToken = exchangeData.access_token
|
||||
const expiresIn = exchangeData.expires_in ?? 5_184_000
|
||||
|
||||
await db
|
||||
.update(schema.account)
|
||||
.set({
|
||||
accessToken: longLivedToken,
|
||||
accessTokenExpiresAt: new Date(Date.now() + expiresIn * 1000),
|
||||
})
|
||||
.where(eq(schema.account.id, account.id))
|
||||
|
||||
logger.info('Exchanged Meta short-lived token for long-lived token', { expiresIn })
|
||||
} else {
|
||||
logger.warn('Failed to exchange Meta token for long-lived token', {
|
||||
status: exchangeResponse.status,
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error exchanging Meta token', { error })
|
||||
}
|
||||
}
|
||||
|
||||
if (isMicrosoftProvider(account.providerId)) {
|
||||
await db
|
||||
.update(schema.account)
|
||||
@@ -474,6 +507,7 @@ export const auth = betterAuth({
|
||||
'shopify',
|
||||
'trello',
|
||||
'calcom',
|
||||
'meta-ads',
|
||||
...SSO_TRUSTED_PROVIDERS,
|
||||
],
|
||||
},
|
||||
@@ -2418,6 +2452,55 @@ export const auth = betterAuth({
|
||||
},
|
||||
},
|
||||
|
||||
// Meta Ads provider
|
||||
{
|
||||
providerId: 'meta-ads',
|
||||
clientId: env.META_ADS_CLIENT_ID as string,
|
||||
clientSecret: env.META_ADS_CLIENT_SECRET as string,
|
||||
authorizationUrl: 'https://www.facebook.com/v24.0/dialog/oauth',
|
||||
tokenUrl: 'https://graph.facebook.com/v24.0/oauth/access_token',
|
||||
userInfoUrl: 'https://graph.facebook.com/v24.0/me',
|
||||
scopes: getCanonicalScopesForProvider('meta-ads'),
|
||||
responseType: 'code',
|
||||
prompt: 'consent',
|
||||
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/meta-ads`,
|
||||
getUserInfo: async (tokens) => {
|
||||
try {
|
||||
logger.info('Fetching Meta user profile')
|
||||
|
||||
const response = await fetch(
|
||||
'https://graph.facebook.com/v24.0/me?fields=id,name,email',
|
||||
{
|
||||
headers: { Authorization: `Bearer ${tokens.accessToken}` },
|
||||
}
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
await response.text().catch(() => {})
|
||||
logger.error('Failed to fetch Meta user info', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
})
|
||||
throw new Error('Failed to fetch user info')
|
||||
}
|
||||
|
||||
const profile = await response.json()
|
||||
|
||||
return {
|
||||
id: `${profile.id}-${crypto.randomUUID()}`,
|
||||
name: profile.name || 'Meta User',
|
||||
email: profile.email || `${profile.id}@facebook.user`,
|
||||
emailVerified: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error in Meta getUserInfo:', { error })
|
||||
return null
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
// Zoom provider
|
||||
{
|
||||
providerId: 'zoom',
|
||||
|
||||
@@ -285,6 +285,8 @@ export const env = createEnv({
|
||||
TRELLO_API_KEY: z.string().optional(), // Trello API Key
|
||||
LINKEDIN_CLIENT_ID: z.string().optional(), // LinkedIn OAuth client ID
|
||||
LINKEDIN_CLIENT_SECRET: z.string().optional(), // LinkedIn OAuth client secret
|
||||
META_ADS_CLIENT_ID: z.string().optional(), // Meta Ads OAuth client ID
|
||||
META_ADS_CLIENT_SECRET: z.string().optional(), // Meta Ads OAuth client secret
|
||||
SHOPIFY_CLIENT_ID: z.string().optional(), // Shopify OAuth client ID
|
||||
SHOPIFY_CLIENT_SECRET: z.string().optional(), // Shopify OAuth client secret
|
||||
ZOOM_CLIENT_ID: z.string().optional(), // Zoom OAuth client ID
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
JiraIcon,
|
||||
LinearIcon,
|
||||
LinkedInIcon,
|
||||
MetaAdsIcon,
|
||||
MicrosoftDataverseIcon,
|
||||
MicrosoftExcelIcon,
|
||||
MicrosoftIcon,
|
||||
@@ -841,6 +842,21 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
|
||||
},
|
||||
defaultService: 'hubspot',
|
||||
},
|
||||
'meta-ads': {
|
||||
name: 'Meta Ads',
|
||||
icon: MetaAdsIcon,
|
||||
services: {
|
||||
'meta-ads': {
|
||||
name: 'Meta Ads',
|
||||
description: 'Query campaigns, ad sets, ads, and performance insights in Meta Ads.',
|
||||
providerId: 'meta-ads',
|
||||
icon: MetaAdsIcon,
|
||||
baseProviderIcon: MetaAdsIcon,
|
||||
scopes: ['ads_read'],
|
||||
},
|
||||
},
|
||||
defaultService: 'meta-ads',
|
||||
},
|
||||
linkedin: {
|
||||
name: 'LinkedIn',
|
||||
icon: LinkedInIcon,
|
||||
@@ -1284,6 +1300,19 @@ function getProviderAuthConfig(provider: string): ProviderAuthConfig {
|
||||
supportsRefreshTokenRotation: false,
|
||||
}
|
||||
}
|
||||
case 'meta-ads': {
|
||||
const { clientId, clientSecret } = getCredentials(
|
||||
env.META_ADS_CLIENT_ID,
|
||||
env.META_ADS_CLIENT_SECRET
|
||||
)
|
||||
return {
|
||||
tokenEndpoint: 'https://graph.facebook.com/v24.0/oauth/access_token',
|
||||
clientId,
|
||||
clientSecret,
|
||||
useBasicAuth: false,
|
||||
supportsRefreshTokenRotation: false,
|
||||
}
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unsupported provider: ${provider}`)
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ export type OAuthProvider =
|
||||
| 'hubspot'
|
||||
| 'salesforce'
|
||||
| 'linkedin'
|
||||
| 'meta-ads'
|
||||
| 'shopify'
|
||||
| 'zoom'
|
||||
| 'wordpress'
|
||||
@@ -89,6 +90,7 @@ export type OAuthService =
|
||||
| 'hubspot'
|
||||
| 'salesforce'
|
||||
| 'linkedin'
|
||||
| 'meta-ads'
|
||||
| 'shopify'
|
||||
| 'zoom'
|
||||
| 'wordpress'
|
||||
|
||||
@@ -333,6 +333,9 @@ export const SCOPE_DESCRIPTIONS: Record<string, string> = {
|
||||
'webhooks:read': 'Read Pipedrive webhooks',
|
||||
'webhooks:full': 'Full access to manage Pipedrive webhooks',
|
||||
|
||||
// Meta Ads scopes
|
||||
ads_read: 'Read ad account data and insights',
|
||||
|
||||
// LinkedIn scopes
|
||||
w_member_social: 'Access LinkedIn profile',
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ export const SELECTOR_CONTEXT_FIELDS = new Set<keyof SelectorContext>([
|
||||
'baseId',
|
||||
'datasetId',
|
||||
'serviceDeskId',
|
||||
'accountId',
|
||||
])
|
||||
|
||||
/**
|
||||
|
||||
98
apps/sim/tools/meta_ads/get_account.ts
Normal file
98
apps/sim/tools/meta_ads/get_account.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import type { MetaAdsGetAccountParams, MetaAdsGetAccountResponse } from '@/tools/meta_ads/types'
|
||||
import { getMetaApiBaseUrl, stripActPrefix } from '@/tools/meta_ads/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const metaAdsGetAccountTool: ToolConfig<MetaAdsGetAccountParams, MetaAdsGetAccountResponse> =
|
||||
{
|
||||
id: 'meta_ads_get_account',
|
||||
name: 'Get Meta Ads Account',
|
||||
description: 'Get information about a Meta Ads account',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'meta-ads',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token for the Meta Marketing API',
|
||||
},
|
||||
accountId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Meta Ads account ID (numeric, without act_ prefix)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const fields =
|
||||
'id,name,account_status,currency,timezone_name,amount_spent,spend_cap,business_country_code'
|
||||
return `${getMetaApiBaseUrl()}/act_${stripActPrefix(params.accountId)}?fields=${fields}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
const errorMessage = data?.error?.message ?? 'Unknown error'
|
||||
return {
|
||||
success: false,
|
||||
output: {
|
||||
id: '',
|
||||
name: '',
|
||||
accountStatus: 0,
|
||||
currency: '',
|
||||
timezone: '',
|
||||
amountSpent: '0',
|
||||
spendCap: null,
|
||||
businessCountryCode: null,
|
||||
},
|
||||
error: errorMessage,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: data.id ?? '',
|
||||
name: data.name ?? '',
|
||||
accountStatus: data.account_status ?? 0,
|
||||
currency: data.currency ?? '',
|
||||
timezone: data.timezone_name ?? '',
|
||||
amountSpent: data.amount_spent ?? '0',
|
||||
spendCap: data.spend_cap ?? null,
|
||||
businessCountryCode: data.business_country_code ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: { type: 'string', description: 'Ad account ID' },
|
||||
name: { type: 'string', description: 'Ad account name' },
|
||||
accountStatus: { type: 'number', description: 'Account status code' },
|
||||
currency: { type: 'string', description: 'Account currency (e.g., USD)' },
|
||||
timezone: { type: 'string', description: 'Account timezone' },
|
||||
amountSpent: { type: 'string', description: 'Total amount spent' },
|
||||
spendCap: {
|
||||
type: 'string',
|
||||
description: 'Spending limit for the account',
|
||||
optional: true,
|
||||
},
|
||||
businessCountryCode: {
|
||||
type: 'string',
|
||||
description: 'Country code for the business',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
214
apps/sim/tools/meta_ads/get_insights.ts
Normal file
214
apps/sim/tools/meta_ads/get_insights.ts
Normal file
@@ -0,0 +1,214 @@
|
||||
import type { MetaAdsGetInsightsParams, MetaAdsGetInsightsResponse } from '@/tools/meta_ads/types'
|
||||
import { getMetaApiBaseUrl, stripActPrefix } from '@/tools/meta_ads/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const metaAdsGetInsightsTool: ToolConfig<
|
||||
MetaAdsGetInsightsParams,
|
||||
MetaAdsGetInsightsResponse
|
||||
> = {
|
||||
id: 'meta_ads_get_insights',
|
||||
name: 'Get Meta Ads Insights',
|
||||
description: 'Get performance insights and metrics for Meta Ads campaigns, ad sets, or ads',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'meta-ads',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token for the Meta Marketing API',
|
||||
},
|
||||
accountId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Meta Ads account ID (numeric, without act_ prefix)',
|
||||
},
|
||||
level: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Aggregation level for insights (account, campaign, adset, ad)',
|
||||
},
|
||||
campaignId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Filter insights by campaign ID',
|
||||
},
|
||||
adSetId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Filter insights by ad set ID',
|
||||
},
|
||||
datePreset: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Predefined date range (today, yesterday, last_7d, last_14d, last_28d, last_30d, last_90d, maximum, this_month, last_month)',
|
||||
},
|
||||
startDate: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Custom start date in YYYY-MM-DD format',
|
||||
},
|
||||
endDate: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Custom end date in YYYY-MM-DD format',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Maximum number of insight rows to return',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const fields =
|
||||
'account_id,campaign_id,campaign_name,adset_id,adset_name,ad_id,ad_name,impressions,clicks,spend,ctr,cpc,cpm,reach,frequency,actions,date_start,date_stop'
|
||||
const searchParams = new URLSearchParams({ fields, level: params.level })
|
||||
|
||||
if (params.startDate && params.endDate) {
|
||||
searchParams.set(
|
||||
'time_range',
|
||||
JSON.stringify({ since: params.startDate, until: params.endDate })
|
||||
)
|
||||
} else if (params.datePreset) {
|
||||
searchParams.set('date_preset', params.datePreset)
|
||||
} else {
|
||||
searchParams.set('date_preset', 'last_30d')
|
||||
}
|
||||
|
||||
if (params.limit) {
|
||||
searchParams.set('limit', String(params.limit))
|
||||
}
|
||||
|
||||
let parentId: string
|
||||
if (params.adSetId) {
|
||||
parentId = params.adSetId.trim()
|
||||
} else if (params.campaignId) {
|
||||
parentId = params.campaignId.trim()
|
||||
} else {
|
||||
parentId = `act_${stripActPrefix(params.accountId)}`
|
||||
}
|
||||
|
||||
return `${getMetaApiBaseUrl()}/${parentId}/insights?${searchParams.toString()}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
const errorMessage = data?.error?.message ?? 'Unknown error'
|
||||
return {
|
||||
success: false,
|
||||
output: { insights: [], totalCount: 0 },
|
||||
error: errorMessage,
|
||||
}
|
||||
}
|
||||
|
||||
const items = data.data ?? []
|
||||
const insights = items.map((i: Record<string, unknown>) => {
|
||||
const actions = (i.actions as Array<Record<string, unknown>>) ?? []
|
||||
const conversionTypes = new Set([
|
||||
'offsite_conversion',
|
||||
'onsite_conversion',
|
||||
'app_custom_event',
|
||||
])
|
||||
const conversionPrefixes = ['offsite_conversion.', 'onsite_conversion.', 'app_custom_event.']
|
||||
const conversions = actions
|
||||
.filter((a) => {
|
||||
const actionType = a.action_type as string
|
||||
return (
|
||||
conversionTypes.has(actionType) ||
|
||||
conversionPrefixes.some((prefix) => actionType?.startsWith(prefix))
|
||||
)
|
||||
})
|
||||
.reduce((sum, a) => sum + Number(a.value ?? 0), 0)
|
||||
|
||||
return {
|
||||
accountId: (i.account_id as string) ?? null,
|
||||
campaignId: (i.campaign_id as string) ?? null,
|
||||
campaignName: (i.campaign_name as string) ?? null,
|
||||
adSetId: (i.adset_id as string) ?? null,
|
||||
adSetName: (i.adset_name as string) ?? null,
|
||||
adId: (i.ad_id as string) ?? null,
|
||||
adName: (i.ad_name as string) ?? null,
|
||||
impressions: (i.impressions as string) ?? '0',
|
||||
clicks: (i.clicks as string) ?? '0',
|
||||
spend: (i.spend as string) ?? '0',
|
||||
ctr: (i.ctr as string) ?? null,
|
||||
cpc: (i.cpc as string) ?? null,
|
||||
cpm: (i.cpm as string) ?? null,
|
||||
reach: (i.reach as string) ?? null,
|
||||
frequency: (i.frequency as string) ?? null,
|
||||
conversions,
|
||||
dateStart: (i.date_start as string) ?? '',
|
||||
dateStop: (i.date_stop as string) ?? '',
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
insights,
|
||||
totalCount: insights.length,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
insights: {
|
||||
type: 'array',
|
||||
description: 'Performance insight rows',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
accountId: { type: 'string', description: 'Ad account ID' },
|
||||
campaignId: { type: 'string', description: 'Campaign ID' },
|
||||
campaignName: { type: 'string', description: 'Campaign name' },
|
||||
adSetId: { type: 'string', description: 'Ad set ID' },
|
||||
adSetName: { type: 'string', description: 'Ad set name' },
|
||||
adId: { type: 'string', description: 'Ad ID' },
|
||||
adName: { type: 'string', description: 'Ad name' },
|
||||
impressions: { type: 'string', description: 'Number of impressions' },
|
||||
clicks: { type: 'string', description: 'Number of clicks' },
|
||||
spend: { type: 'string', description: 'Amount spent in account currency' },
|
||||
ctr: { type: 'string', description: 'Click-through rate' },
|
||||
cpc: { type: 'string', description: 'Cost per click' },
|
||||
cpm: { type: 'string', description: 'Cost per 1,000 impressions' },
|
||||
reach: { type: 'string', description: 'Number of unique users reached' },
|
||||
frequency: {
|
||||
type: 'string',
|
||||
description: 'Average number of times each person saw the ad',
|
||||
},
|
||||
conversions: { type: 'number', description: 'Total conversions from actions' },
|
||||
dateStart: { type: 'string', description: 'Start date of the reporting period' },
|
||||
dateStop: { type: 'string', description: 'End date of the reporting period' },
|
||||
},
|
||||
},
|
||||
},
|
||||
totalCount: {
|
||||
type: 'number',
|
||||
description:
|
||||
'Number of insight rows returned in this response (may be limited by pagination)',
|
||||
},
|
||||
},
|
||||
}
|
||||
15
apps/sim/tools/meta_ads/index.ts
Normal file
15
apps/sim/tools/meta_ads/index.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { metaAdsGetAccountTool } from '@/tools/meta_ads/get_account'
|
||||
import { metaAdsGetInsightsTool } from '@/tools/meta_ads/get_insights'
|
||||
import { metaAdsListAdSetsTool } from '@/tools/meta_ads/list_ad_sets'
|
||||
import { metaAdsListAdsTool } from '@/tools/meta_ads/list_ads'
|
||||
import { metaAdsListCampaignsTool } from '@/tools/meta_ads/list_campaigns'
|
||||
|
||||
export {
|
||||
metaAdsGetAccountTool,
|
||||
metaAdsGetInsightsTool,
|
||||
metaAdsListAdSetsTool,
|
||||
metaAdsListAdsTool,
|
||||
metaAdsListCampaignsTool,
|
||||
}
|
||||
|
||||
export * from './types'
|
||||
130
apps/sim/tools/meta_ads/list_ad_sets.ts
Normal file
130
apps/sim/tools/meta_ads/list_ad_sets.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import type { MetaAdsListAdSetsParams, MetaAdsListAdSetsResponse } from '@/tools/meta_ads/types'
|
||||
import { getMetaApiBaseUrl, stripActPrefix } from '@/tools/meta_ads/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const metaAdsListAdSetsTool: ToolConfig<MetaAdsListAdSetsParams, MetaAdsListAdSetsResponse> =
|
||||
{
|
||||
id: 'meta_ads_list_ad_sets',
|
||||
name: 'List Meta Ads Ad Sets',
|
||||
description: 'List ad sets in a Meta Ads account or campaign',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'meta-ads',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token for the Meta Marketing API',
|
||||
},
|
||||
accountId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Meta Ads account ID (numeric, without act_ prefix)',
|
||||
},
|
||||
campaignId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Filter ad sets by campaign ID',
|
||||
},
|
||||
status: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Filter by ad set status (ACTIVE, PAUSED, ARCHIVED, DELETED)',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Maximum number of ad sets to return',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const fields = 'id,name,status,campaign_id,daily_budget,lifetime_budget,start_time,end_time'
|
||||
const searchParams = new URLSearchParams({ fields })
|
||||
|
||||
if (params.status) {
|
||||
searchParams.set('effective_status', JSON.stringify([params.status]))
|
||||
}
|
||||
if (params.limit) {
|
||||
searchParams.set('limit', String(params.limit))
|
||||
}
|
||||
|
||||
const parentId = params.campaignId?.trim() || `act_${stripActPrefix(params.accountId)}`
|
||||
return `${getMetaApiBaseUrl()}/${parentId}/adsets?${searchParams.toString()}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
const errorMessage = data?.error?.message ?? 'Unknown error'
|
||||
return {
|
||||
success: false,
|
||||
output: { adSets: [], totalCount: 0 },
|
||||
error: errorMessage,
|
||||
}
|
||||
}
|
||||
|
||||
const items = data.data ?? []
|
||||
const adSets = items.map((a: Record<string, unknown>) => ({
|
||||
id: (a.id as string) ?? '',
|
||||
name: (a.name as string) ?? '',
|
||||
status: (a.status as string) ?? '',
|
||||
campaignId: (a.campaign_id as string) ?? '',
|
||||
dailyBudget: (a.daily_budget as string) ?? null,
|
||||
lifetimeBudget: (a.lifetime_budget as string) ?? null,
|
||||
startTime: (a.start_time as string) ?? null,
|
||||
endTime: (a.end_time as string) ?? null,
|
||||
}))
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
adSets,
|
||||
totalCount: adSets.length,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
adSets: {
|
||||
type: 'array',
|
||||
description: 'List of ad sets',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Ad set ID' },
|
||||
name: { type: 'string', description: 'Ad set name' },
|
||||
status: { type: 'string', description: 'Ad set status' },
|
||||
campaignId: { type: 'string', description: 'Parent campaign ID' },
|
||||
dailyBudget: { type: 'string', description: 'Daily budget in account currency cents' },
|
||||
lifetimeBudget: {
|
||||
type: 'string',
|
||||
description: 'Lifetime budget in account currency cents',
|
||||
},
|
||||
startTime: { type: 'string', description: 'Ad set start time (ISO 8601)' },
|
||||
endTime: { type: 'string', description: 'Ad set end time (ISO 8601)' },
|
||||
},
|
||||
},
|
||||
},
|
||||
totalCount: {
|
||||
type: 'number',
|
||||
description: 'Number of ad sets returned in this response (may be limited by pagination)',
|
||||
},
|
||||
},
|
||||
}
|
||||
138
apps/sim/tools/meta_ads/list_ads.ts
Normal file
138
apps/sim/tools/meta_ads/list_ads.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import type { MetaAdsListAdsParams, MetaAdsListAdsResponse } from '@/tools/meta_ads/types'
|
||||
import { getMetaApiBaseUrl, stripActPrefix } from '@/tools/meta_ads/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const metaAdsListAdsTool: ToolConfig<MetaAdsListAdsParams, MetaAdsListAdsResponse> = {
|
||||
id: 'meta_ads_list_ads',
|
||||
name: 'List Meta Ads',
|
||||
description: 'List ads in a Meta Ads account, campaign, or ad set',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'meta-ads',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token for the Meta Marketing API',
|
||||
},
|
||||
accountId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Meta Ads account ID (numeric, without act_ prefix)',
|
||||
},
|
||||
campaignId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Filter ads by campaign ID',
|
||||
},
|
||||
adSetId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Filter ads by ad set ID',
|
||||
},
|
||||
status: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Filter by ad status (ACTIVE, PAUSED, ARCHIVED, DELETED)',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Maximum number of ads to return',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const fields = 'id,name,status,adset_id,campaign_id,created_time,updated_time'
|
||||
const searchParams = new URLSearchParams({ fields })
|
||||
|
||||
if (params.status) {
|
||||
searchParams.set('effective_status', JSON.stringify([params.status]))
|
||||
}
|
||||
if (params.limit) {
|
||||
searchParams.set('limit', String(params.limit))
|
||||
}
|
||||
|
||||
let parentId: string
|
||||
if (params.adSetId) {
|
||||
parentId = params.adSetId.trim()
|
||||
} else if (params.campaignId) {
|
||||
parentId = params.campaignId.trim()
|
||||
} else {
|
||||
parentId = `act_${stripActPrefix(params.accountId)}`
|
||||
}
|
||||
|
||||
return `${getMetaApiBaseUrl()}/${parentId}/ads?${searchParams.toString()}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
const errorMessage = data?.error?.message ?? 'Unknown error'
|
||||
return {
|
||||
success: false,
|
||||
output: { ads: [], totalCount: 0 },
|
||||
error: errorMessage,
|
||||
}
|
||||
}
|
||||
|
||||
const items = data.data ?? []
|
||||
const ads = items.map((a: Record<string, unknown>) => ({
|
||||
id: (a.id as string) ?? '',
|
||||
name: (a.name as string) ?? '',
|
||||
status: (a.status as string) ?? '',
|
||||
adSetId: (a.adset_id as string) ?? null,
|
||||
campaignId: (a.campaign_id as string) ?? null,
|
||||
createdTime: (a.created_time as string) ?? null,
|
||||
updatedTime: (a.updated_time as string) ?? null,
|
||||
}))
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
ads,
|
||||
totalCount: ads.length,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
ads: {
|
||||
type: 'array',
|
||||
description: 'List of ads',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Ad ID' },
|
||||
name: { type: 'string', description: 'Ad name' },
|
||||
status: { type: 'string', description: 'Ad status' },
|
||||
adSetId: { type: 'string', description: 'Parent ad set ID' },
|
||||
campaignId: { type: 'string', description: 'Parent campaign ID' },
|
||||
createdTime: { type: 'string', description: 'Ad creation time (ISO 8601)' },
|
||||
updatedTime: { type: 'string', description: 'Ad last update time (ISO 8601)' },
|
||||
},
|
||||
},
|
||||
},
|
||||
totalCount: {
|
||||
type: 'number',
|
||||
description: 'Number of ads returned in this response (may be limited by pagination)',
|
||||
},
|
||||
},
|
||||
}
|
||||
129
apps/sim/tools/meta_ads/list_campaigns.ts
Normal file
129
apps/sim/tools/meta_ads/list_campaigns.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
import type {
|
||||
MetaAdsListCampaignsParams,
|
||||
MetaAdsListCampaignsResponse,
|
||||
} from '@/tools/meta_ads/types'
|
||||
import { getMetaApiBaseUrl, stripActPrefix } from '@/tools/meta_ads/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const metaAdsListCampaignsTool: ToolConfig<
|
||||
MetaAdsListCampaignsParams,
|
||||
MetaAdsListCampaignsResponse
|
||||
> = {
|
||||
id: 'meta_ads_list_campaigns',
|
||||
name: 'List Meta Ads Campaigns',
|
||||
description: 'List campaigns in a Meta Ads account with optional status filtering',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'meta-ads',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token for the Meta Marketing API',
|
||||
},
|
||||
accountId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Meta Ads account ID (numeric, without act_ prefix)',
|
||||
},
|
||||
status: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Filter by campaign status (ACTIVE, PAUSED, ARCHIVED, DELETED)',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Maximum number of campaigns to return',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const fields =
|
||||
'id,name,status,objective,daily_budget,lifetime_budget,created_time,updated_time'
|
||||
const searchParams = new URLSearchParams({ fields })
|
||||
|
||||
if (params.status) {
|
||||
searchParams.set('effective_status', JSON.stringify([params.status]))
|
||||
}
|
||||
if (params.limit) {
|
||||
searchParams.set('limit', String(params.limit))
|
||||
}
|
||||
|
||||
return `${getMetaApiBaseUrl()}/act_${stripActPrefix(params.accountId)}/campaigns?${searchParams.toString()}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
const errorMessage = data?.error?.message ?? 'Unknown error'
|
||||
return {
|
||||
success: false,
|
||||
output: { campaigns: [], totalCount: 0 },
|
||||
error: errorMessage,
|
||||
}
|
||||
}
|
||||
|
||||
const items = data.data ?? []
|
||||
const campaigns = items.map((c: Record<string, unknown>) => ({
|
||||
id: (c.id as string) ?? '',
|
||||
name: (c.name as string) ?? '',
|
||||
status: (c.status as string) ?? '',
|
||||
objective: (c.objective as string) ?? null,
|
||||
dailyBudget: (c.daily_budget as string) ?? null,
|
||||
lifetimeBudget: (c.lifetime_budget as string) ?? null,
|
||||
createdTime: (c.created_time as string) ?? null,
|
||||
updatedTime: (c.updated_time as string) ?? null,
|
||||
}))
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
campaigns,
|
||||
totalCount: campaigns.length,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
campaigns: {
|
||||
type: 'array',
|
||||
description: 'List of campaigns in the account',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Campaign ID' },
|
||||
name: { type: 'string', description: 'Campaign name' },
|
||||
status: { type: 'string', description: 'Campaign status (ACTIVE, PAUSED, etc.)' },
|
||||
objective: { type: 'string', description: 'Campaign objective' },
|
||||
dailyBudget: { type: 'string', description: 'Daily budget in account currency cents' },
|
||||
lifetimeBudget: {
|
||||
type: 'string',
|
||||
description: 'Lifetime budget in account currency cents',
|
||||
},
|
||||
createdTime: { type: 'string', description: 'Campaign creation time (ISO 8601)' },
|
||||
updatedTime: { type: 'string', description: 'Campaign last update time (ISO 8601)' },
|
||||
},
|
||||
},
|
||||
},
|
||||
totalCount: {
|
||||
type: 'number',
|
||||
description: 'Number of campaigns returned in this response (may be limited by pagination)',
|
||||
},
|
||||
},
|
||||
}
|
||||
147
apps/sim/tools/meta_ads/types.ts
Normal file
147
apps/sim/tools/meta_ads/types.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import type { ToolResponse } from '@/tools/types'
|
||||
|
||||
const META_API_VERSION = 'v24.0'
|
||||
|
||||
export function getMetaApiBaseUrl(): string {
|
||||
return `https://graph.facebook.com/${META_API_VERSION}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Strips the `act_` prefix from an account ID if present, so that
|
||||
* callers can safely wrap the result with `act_` without double-prefixing.
|
||||
*/
|
||||
export function stripActPrefix(accountId: string): string {
|
||||
const trimmed = accountId.trim()
|
||||
return trimmed.startsWith('act_') ? trimmed.slice(4) : trimmed
|
||||
}
|
||||
|
||||
export interface MetaAdsBaseParams {
|
||||
accessToken: string
|
||||
accountId: string
|
||||
}
|
||||
|
||||
export interface MetaAdsGetAccountParams extends MetaAdsBaseParams {}
|
||||
|
||||
export interface MetaAdsListCampaignsParams extends MetaAdsBaseParams {
|
||||
status?: string
|
||||
limit?: number
|
||||
}
|
||||
|
||||
export interface MetaAdsListAdSetsParams extends MetaAdsBaseParams {
|
||||
campaignId?: string
|
||||
status?: string
|
||||
limit?: number
|
||||
}
|
||||
|
||||
export interface MetaAdsListAdsParams extends MetaAdsBaseParams {
|
||||
campaignId?: string
|
||||
adSetId?: string
|
||||
status?: string
|
||||
limit?: number
|
||||
}
|
||||
|
||||
export interface MetaAdsGetInsightsParams extends MetaAdsBaseParams {
|
||||
level: string
|
||||
campaignId?: string
|
||||
adSetId?: string
|
||||
datePreset?: string
|
||||
startDate?: string
|
||||
endDate?: string
|
||||
limit?: number
|
||||
}
|
||||
|
||||
export interface MetaAdsAccount {
|
||||
id: string
|
||||
name: string
|
||||
accountStatus: number
|
||||
currency: string
|
||||
timezone: string
|
||||
amountSpent: string
|
||||
spendCap: string | null
|
||||
businessCountryCode: string | null
|
||||
}
|
||||
|
||||
export interface MetaAdsGetAccountResponse extends ToolResponse {
|
||||
output: MetaAdsAccount
|
||||
}
|
||||
|
||||
export interface MetaAdsCampaign {
|
||||
id: string
|
||||
name: string
|
||||
status: string
|
||||
objective: string | null
|
||||
dailyBudget: string | null
|
||||
lifetimeBudget: string | null
|
||||
createdTime: string | null
|
||||
updatedTime: string | null
|
||||
}
|
||||
|
||||
export interface MetaAdsListCampaignsResponse extends ToolResponse {
|
||||
output: {
|
||||
campaigns: MetaAdsCampaign[]
|
||||
totalCount: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface MetaAdsAdSet {
|
||||
id: string
|
||||
name: string
|
||||
status: string
|
||||
campaignId: string
|
||||
dailyBudget: string | null
|
||||
lifetimeBudget: string | null
|
||||
startTime: string | null
|
||||
endTime: string | null
|
||||
}
|
||||
|
||||
export interface MetaAdsListAdSetsResponse extends ToolResponse {
|
||||
output: {
|
||||
adSets: MetaAdsAdSet[]
|
||||
totalCount: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface MetaAdsAd {
|
||||
id: string
|
||||
name: string
|
||||
status: string
|
||||
adSetId: string | null
|
||||
campaignId: string | null
|
||||
createdTime: string | null
|
||||
updatedTime: string | null
|
||||
}
|
||||
|
||||
export interface MetaAdsListAdsResponse extends ToolResponse {
|
||||
output: {
|
||||
ads: MetaAdsAd[]
|
||||
totalCount: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface MetaAdsInsight {
|
||||
accountId: string | null
|
||||
campaignId: string | null
|
||||
campaignName: string | null
|
||||
adSetId: string | null
|
||||
adSetName: string | null
|
||||
adId: string | null
|
||||
adName: string | null
|
||||
impressions: string
|
||||
clicks: string
|
||||
spend: string
|
||||
ctr: string | null
|
||||
cpc: string | null
|
||||
cpm: string | null
|
||||
reach: string | null
|
||||
frequency: string | null
|
||||
conversions: number
|
||||
dateStart: string
|
||||
dateStop: string
|
||||
}
|
||||
|
||||
export interface MetaAdsGetInsightsResponse extends ToolResponse {
|
||||
output: {
|
||||
insights: MetaAdsInsight[]
|
||||
totalCount: number
|
||||
}
|
||||
}
|
||||
@@ -1388,6 +1388,13 @@ import {
|
||||
} from '@/tools/mailgun'
|
||||
import { mem0AddMemoriesTool, mem0GetMemoriesTool, mem0SearchMemoriesTool } from '@/tools/mem0'
|
||||
import { memoryAddTool, memoryDeleteTool, memoryGetAllTool, memoryGetTool } from '@/tools/memory'
|
||||
import {
|
||||
metaAdsGetAccountTool,
|
||||
metaAdsGetInsightsTool,
|
||||
metaAdsListAdSetsTool,
|
||||
metaAdsListAdsTool,
|
||||
metaAdsListCampaignsTool,
|
||||
} from '@/tools/meta_ads'
|
||||
import {
|
||||
dataverseAssociateTool,
|
||||
dataverseCreateMultipleTool,
|
||||
@@ -3677,6 +3684,11 @@ export const tools: Record<string, ToolConfig> = {
|
||||
mem0_add_memories: mem0AddMemoriesTool,
|
||||
mem0_search_memories: mem0SearchMemoriesTool,
|
||||
mem0_get_memories: mem0GetMemoriesTool,
|
||||
meta_ads_get_account: metaAdsGetAccountTool,
|
||||
meta_ads_get_insights: metaAdsGetInsightsTool,
|
||||
meta_ads_list_ad_sets: metaAdsListAdSetsTool,
|
||||
meta_ads_list_ads: metaAdsListAdsTool,
|
||||
meta_ads_list_campaigns: metaAdsListCampaignsTool,
|
||||
zep_create_thread: zepCreateThreadTool,
|
||||
zep_get_threads: zepGetThreadsTool,
|
||||
zep_delete_thread: zepDeleteThreadTool,
|
||||
|
||||
Reference in New Issue
Block a user