mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
feat(meta-ads): add meta ads integration for campaign and ad performance queries
- Add 5 tools: get_account, list_campaigns, list_ad_sets, list_ads, get_insights - Add account and campaign selectors with cascading dropdown support - Add OAuth config with ads_read scope - Generate docs
This commit is contained in:
@@ -4135,6 +4135,52 @@ export function LumaIcon(props: SVGProps<SVGSVGElement>) {
|
||||
)
|
||||
}
|
||||
|
||||
export function MetaAdsIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 287.56 191'>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id='meta-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='meta-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(#meta-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(#meta-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",
|
||||
|
||||
169
apps/docs/content/docs/en/tools/meta_ads.mdx
Normal file
169
apps/docs/content/docs/en/tools/meta_ads.mdx
Normal file
@@ -0,0 +1,169 @@
|
||||
---
|
||||
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"
|
||||
/>
|
||||
|
||||
## 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 | Total number of campaigns returned |
|
||||
|
||||
### `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 | Total number of ad sets returned |
|
||||
|
||||
### `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 | Total number of ads returned |
|
||||
|
||||
### `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, 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 | Total number of insight rows returned |
|
||||
|
||||
|
||||
90
apps/sim/app/api/tools/meta_ads/accounts/route.ts
Normal file
90
apps/sim/app/api/tools/meta_ads/accounts/route.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
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 accounts = items
|
||||
.filter((account) => account.account_status === 1)
|
||||
.map((account) => ({
|
||||
id: account.account_id,
|
||||
name: account.name || `Account ${account.account_id}`,
|
||||
}))
|
||||
|
||||
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 } 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 = String(accountId).trim()
|
||||
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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
267
apps/sim/blocks/blocks/meta_ads.ts
Normal file
267
apps/sim/blocks/blocks/meta_ads.ts
Normal file
@@ -0,0 +1,267 @@
|
||||
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' },
|
||||
],
|
||||
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,52 @@ export function LumaIcon(props: SVGProps<SVGSVGElement>) {
|
||||
)
|
||||
}
|
||||
|
||||
export function MetaAdsIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 287.56 191'>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id='meta-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='meta-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(#meta-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(#meta-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 {
|
||||
|
||||
@@ -474,6 +474,7 @@ export const auth = betterAuth({
|
||||
'shopify',
|
||||
'trello',
|
||||
'calcom',
|
||||
'meta-ads',
|
||||
...SSO_TRUSTED_PROVIDERS,
|
||||
],
|
||||
},
|
||||
@@ -2418,6 +2419,52 @@ 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&access_token=${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 } 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_${params.accountId.trim()}?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,
|
||||
},
|
||||
},
|
||||
}
|
||||
199
apps/sim/tools/meta_ads/get_insights.ts
Normal file
199
apps/sim/tools/meta_ads/get_insights.ts
Normal file
@@ -0,0 +1,199 @@
|
||||
import type { MetaAdsGetInsightsParams, MetaAdsGetInsightsResponse } from '@/tools/meta_ads/types'
|
||||
import { getMetaApiBaseUrl } 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, 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_${params.accountId.trim()}`
|
||||
}
|
||||
|
||||
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 conversions = actions.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: 'Total number of insight rows returned',
|
||||
},
|
||||
},
|
||||
}
|
||||
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 } 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_${params.accountId.trim()}`
|
||||
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: 'Total number of ad sets returned',
|
||||
},
|
||||
},
|
||||
}
|
||||
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 } 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_${params.accountId.trim()}`
|
||||
}
|
||||
|
||||
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: 'Total number of ads returned',
|
||||
},
|
||||
},
|
||||
}
|
||||
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 } 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_${params.accountId.trim()}/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: 'Total number of campaigns returned',
|
||||
},
|
||||
},
|
||||
}
|
||||
141
apps/sim/tools/meta_ads/types.ts
Normal file
141
apps/sim/tools/meta_ads/types.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
import type { ToolResponse } from '@/tools/types'
|
||||
|
||||
const META_API_VERSION = 'v24.0'
|
||||
|
||||
export function getMetaApiBaseUrl(): string {
|
||||
return `https://graph.facebook.com/${META_API_VERSION}`
|
||||
}
|
||||
|
||||
export interface MetaAdsBaseParams {
|
||||
accessToken: string
|
||||
accountId: string
|
||||
}
|
||||
|
||||
export interface MetaAdsGetAccountParams {
|
||||
accessToken: string
|
||||
accountId: string
|
||||
}
|
||||
|
||||
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