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:
Waleed Latif
2026-03-13 03:22:21 -07:00
parent 8f15be23a0
commit 3eca67c4ac
26 changed files with 1741 additions and 15 deletions

View File

@@ -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

View File

@@ -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,

View File

@@ -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`

View File

@@ -92,6 +92,7 @@
"mailgun",
"mem0",
"memory",
"meta_ads",
"microsoft_dataverse",
"microsoft_excel",
"microsoft_planner",

View 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 |

View 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 }
)
}
}

View 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 }
)
}
}

View 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' },
},
}

View File

@@ -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,

View File

@@ -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

View File

@@ -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,

View File

@@ -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 {

View File

@@ -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',

View File

@@ -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

View File

@@ -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}`)
}

View File

@@ -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'

View File

@@ -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',

View File

@@ -21,6 +21,7 @@ export const SELECTOR_CONTEXT_FIELDS = new Set<keyof SelectorContext>([
'baseId',
'datasetId',
'serviceDeskId',
'accountId',
])
/**

View 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,
},
},
}

View 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',
},
},
}

View 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'

View 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',
},
},
}

View 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',
},
},
}

View 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',
},
},
}

View 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
}
}

View File

@@ -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,