Compare commits

...

9 Commits

Author SHA1 Message Date
Waleed Latif
b2bd567b2e fix(meta-ads): include onsite_conversion and app_custom_event subtypes in conversions
The conversion filter was only matching offsite_conversion.* subtypes but
missing onsite_conversion.* and app_custom_event.* subtypes, which the Meta
API commonly returns at the subtype level.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 09:10:21 -07:00
Waleed Latif
478bcc06fe fix(meta-ads): exchange short-lived token for long-lived token on OAuth connect
Meta's auth code flow returns a short-lived token (~1-2h) with no refresh token.
Add fb_exchange_token call in account.create.after hook to exchange for a
long-lived token (~60 days), following the same pattern as Salesforce's
post-connect token handling.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 07:35:03 -07:00
Waleed Latif
b339b483f0 fix(meta-ads): address greptile review — prefix guard, pagination clarity, account statuses, DELETED filter
- Add stripActPrefix() helper to prevent act_ double-prefix bug when users provide prefixed IDs
- Clarify totalCount descriptions to indicate response-level count (not total in account)
- Show all ad accounts in selector with status badges instead of silently filtering to active only
- Add DELETED to status filter dropdown options

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 07:04:11 -07:00
Waleed Latif
320283a35f fix(meta-ads): use unique gradient IDs and filter conversion actions
- Use useId() for MetaAdsIcon SVG gradient IDs to prevent collisions when multiple instances render on the same page
- Filter conversions to only count actual conversion action types (offsite_conversion, onsite_conversion, app_custom_event) instead of summing all actions
2026-03-14 06:52:09 -07:00
Waleed Latif
bb8b314515 lint 2026-03-14 06:28:24 -07:00
Waleed Latif
698b65f847 refactor(meta-ads): extend MetaAdsBaseParams instead of duplicating fields 2026-03-14 06:28:24 -07:00
Waleed Latif
67fa7b628f fix(meta-ads): use Authorization header for getUserInfo and add maximum date preset to docs
- Pass access token via Authorization header instead of URL query param in getUserInfo, matching all other providers
- Add missing 'maximum' date preset to tool param description and docs
2026-03-14 06:28:24 -07:00
Waleed Latif
e5fe85c44a docs(meta-ads): add manual description section to docs 2026-03-14 06:28:24 -07:00
Waleed Latif
3eca67c4ac 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
2026-03-14 06:28:23 -07:00
26 changed files with 1827 additions and 15 deletions

View File

@@ -4135,6 +4135,55 @@ export function LumaIcon(props: SVGProps<SVGSVGElement>) {
)
}
export function MetaAdsIcon(props: SVGProps<SVGSVGElement>) {
const id = useId()
const lg1 = `${id}-meta-lg1`
const lg2 = `${id}-meta-lg2`
return (
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 287.56 191'>
<defs>
<linearGradient
id={lg1}
x1='62.34'
y1='101.45'
x2='260.34'
y2='91.45'
gradientTransform='matrix(1, 0, 0, -1, 0, 192)'
gradientUnits='userSpaceOnUse'
>
<stop offset='0' stopColor='#0064e1' />
<stop offset='0.4' stopColor='#0064e1' />
<stop offset='0.83' stopColor='#0073ee' />
<stop offset='1' stopColor='#0082fb' />
</linearGradient>
<linearGradient
id={lg2}
x1='41.42'
y1='53'
x2='41.42'
y2='126'
gradientTransform='matrix(1, 0, 0, -1, 0, 192)'
gradientUnits='userSpaceOnUse'
>
<stop offset='0' stopColor='#0082fb' />
<stop offset='1' stopColor='#0064e0' />
</linearGradient>
</defs>
<path
fill='#0081fb'
d='M31.06,126c0,11,2.41,19.41,5.56,24.51A19,19,0,0,0,53.19,160c8.1,0,15.51-2,29.79-21.76,11.44-15.83,24.92-38,34-52l15.36-23.6c10.67-16.39,23-34.61,37.18-47C181.07,5.6,193.54,0,206.09,0c21.07,0,41.14,12.21,56.5,35.11,16.81,25.08,25,56.67,25,89.27,0,19.38-3.82,33.62-10.32,44.87C271,180.13,258.72,191,238.13,191V160c17.63,0,22-16.2,22-34.74,0-26.42-6.16-55.74-19.73-76.69-9.63-14.86-22.11-23.94-35.84-23.94-14.85,0-26.8,11.2-40.23,31.17-7.14,10.61-14.47,23.54-22.7,38.13l-9.06,16c-18.2,32.27-22.81,39.62-31.91,51.75C84.74,183,71.12,191,53.19,191c-21.27,0-34.72-9.21-43-23.09C3.34,156.6,0,141.76,0,124.85Z'
/>
<path
fill={`url(#${lg1})`}
d='M24.49,37.3C38.73,15.35,59.28,0,82.85,0c13.65,0,27.22,4,41.39,15.61,15.5,12.65,32,33.48,52.63,67.81l7.39,12.32c17.84,29.72,28,45,33.93,52.22,7.64,9.26,13,12,19.94,12,17.63,0,22-16.2,22-34.74l27.4-.86c0,19.38-3.82,33.62-10.32,44.87C271,180.13,258.72,191,238.13,191c-12.8,0-24.14-2.78-36.68-14.61-9.64-9.08-20.91-25.21-29.58-39.71L146.08,93.6c-12.94-21.62-24.81-37.74-31.68-45C107,40.71,97.51,31.23,82.35,31.23c-12.27,0-22.69,8.61-31.41,21.78Z'
/>
<path
fill={`url(#${lg2})`}
d='M82.35,31.23c-12.27,0-22.69,8.61-31.41,21.78C38.61,71.62,31.06,99.34,31.06,126c0,11,2.41,19.41,5.56,24.51L10.14,167.91C3.34,156.6,0,141.76,0,124.85,0,94.1,8.44,62.05,24.49,37.3,38.73,15.35,59.28,0,82.85,0Z'
/>
</svg>
)
}
export function MailchimpIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg

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,176 @@
---
title: Meta Ads
description: Query campaigns, ad sets, ads, and performance insights
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="meta_ads"
color="#1877F2"
/>
{/* MANUAL-CONTENT-START:intro */}
[Meta Ads](https://www.facebook.com/business/ads) is Meta's advertising platform that lets businesses create and manage ads across Facebook, Instagram, Messenger, and the Audience Network. It offers campaign management at multiple levels — campaigns, ad sets, and individual ads — with detailed targeting, budgeting, and performance analytics.
In Sim, the Meta Ads integration enables your agents to query account information, browse campaign hierarchies, and pull performance insights including impressions, clicks, spend, CTR, CPC, CPM, reach, frequency, and conversions. This supports use cases such as automated performance reporting, spend monitoring, campaign auditing, and cross-channel analytics workflows. Account and campaign selectors provide a dropdown experience so agents and users can pick resources without manually entering IDs.
{/* MANUAL-CONTENT-END */}
## Usage Instructions
Connect to Meta Ads to view account info, list campaigns, ad sets, and ads, and get performance insights and metrics.
## Tools
### `meta_ads_get_account`
Get information about a Meta Ads account
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `accountId` | string | Yes | Meta Ads account ID \(numeric, without act_ prefix\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `id` | string | Ad account ID |
| `name` | string | Ad account name |
| `accountStatus` | number | Account status code |
| `currency` | string | Account currency \(e.g., USD\) |
| `timezone` | string | Account timezone |
| `amountSpent` | string | Total amount spent |
| `spendCap` | string | Spending limit for the account |
| `businessCountryCode` | string | Country code for the business |
### `meta_ads_list_campaigns`
List campaigns in a Meta Ads account with optional status filtering
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `accountId` | string | Yes | Meta Ads account ID \(numeric, without act_ prefix\) |
| `status` | string | No | Filter by campaign status \(ACTIVE, PAUSED, ARCHIVED, DELETED\) |
| `limit` | number | No | Maximum number of campaigns to return |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `campaigns` | array | List of campaigns in the account |
| ↳ `id` | string | Campaign ID |
| ↳ `name` | string | Campaign name |
| ↳ `status` | string | Campaign status \(ACTIVE, PAUSED, etc.\) |
| ↳ `objective` | string | Campaign objective |
| ↳ `dailyBudget` | string | Daily budget in account currency cents |
| ↳ `lifetimeBudget` | string | Lifetime budget in account currency cents |
| ↳ `createdTime` | string | Campaign creation time \(ISO 8601\) |
| ↳ `updatedTime` | string | Campaign last update time \(ISO 8601\) |
| `totalCount` | number | Number of campaigns returned in this response \(may be limited by pagination\) |
### `meta_ads_list_ad_sets`
List ad sets in a Meta Ads account or campaign
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `accountId` | string | Yes | Meta Ads account ID \(numeric, without act_ prefix\) |
| `campaignId` | string | No | Filter ad sets by campaign ID |
| `status` | string | No | Filter by ad set status \(ACTIVE, PAUSED, ARCHIVED, DELETED\) |
| `limit` | number | No | Maximum number of ad sets to return |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `adSets` | array | List of ad sets |
| ↳ `id` | string | Ad set ID |
| ↳ `name` | string | Ad set name |
| ↳ `status` | string | Ad set status |
| ↳ `campaignId` | string | Parent campaign ID |
| ↳ `dailyBudget` | string | Daily budget in account currency cents |
| ↳ `lifetimeBudget` | string | Lifetime budget in account currency cents |
| ↳ `startTime` | string | Ad set start time \(ISO 8601\) |
| ↳ `endTime` | string | Ad set end time \(ISO 8601\) |
| `totalCount` | number | Number of ad sets returned in this response \(may be limited by pagination\) |
### `meta_ads_list_ads`
List ads in a Meta Ads account, campaign, or ad set
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `accountId` | string | Yes | Meta Ads account ID \(numeric, without act_ prefix\) |
| `campaignId` | string | No | Filter ads by campaign ID |
| `adSetId` | string | No | Filter ads by ad set ID |
| `status` | string | No | Filter by ad status \(ACTIVE, PAUSED, ARCHIVED, DELETED\) |
| `limit` | number | No | Maximum number of ads to return |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `ads` | array | List of ads |
| ↳ `id` | string | Ad ID |
| ↳ `name` | string | Ad name |
| ↳ `status` | string | Ad status |
| ↳ `adSetId` | string | Parent ad set ID |
| ↳ `campaignId` | string | Parent campaign ID |
| ↳ `createdTime` | string | Ad creation time \(ISO 8601\) |
| ↳ `updatedTime` | string | Ad last update time \(ISO 8601\) |
| `totalCount` | number | Number of ads returned in this response \(may be limited by pagination\) |
### `meta_ads_get_insights`
Get performance insights and metrics for Meta Ads campaigns, ad sets, or ads
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `accountId` | string | Yes | Meta Ads account ID \(numeric, without act_ prefix\) |
| `level` | string | Yes | Aggregation level for insights \(account, campaign, adset, ad\) |
| `campaignId` | string | No | Filter insights by campaign ID |
| `adSetId` | string | No | Filter insights by ad set ID |
| `datePreset` | string | No | Predefined date range \(today, yesterday, last_7d, last_14d, last_28d, last_30d, last_90d, maximum, this_month, last_month\) |
| `startDate` | string | No | Custom start date in YYYY-MM-DD format |
| `endDate` | string | No | Custom end date in YYYY-MM-DD format |
| `limit` | number | No | Maximum number of insight rows to return |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `insights` | array | Performance insight rows |
| ↳ `accountId` | string | Ad account ID |
| ↳ `campaignId` | string | Campaign ID |
| ↳ `campaignName` | string | Campaign name |
| ↳ `adSetId` | string | Ad set ID |
| ↳ `adSetName` | string | Ad set name |
| ↳ `adId` | string | Ad ID |
| ↳ `adName` | string | Ad name |
| ↳ `impressions` | string | Number of impressions |
| ↳ `clicks` | string | Number of clicks |
| ↳ `spend` | string | Amount spent in account currency |
| ↳ `ctr` | string | Click-through rate |
| ↳ `cpc` | string | Cost per click |
| ↳ `cpm` | string | Cost per 1,000 impressions |
| ↳ `reach` | string | Number of unique users reached |
| ↳ `frequency` | string | Average number of times each person saw the ad |
| ↳ `conversions` | number | Total conversions from actions |
| ↳ `dateStart` | string | Start date of the reporting period |
| ↳ `dateStop` | string | End date of the reporting period |
| `totalCount` | number | Number of insight rows returned in this response \(may be limited by pagination\) |

View File

@@ -0,0 +1,105 @@
import { createLogger } from '@sim/logger'
import { NextResponse } from 'next/server'
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
import { generateRequestId } from '@/lib/core/utils/request'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
import { getMetaApiBaseUrl } from '@/tools/meta_ads/types'
export const dynamic = 'force-dynamic'
const logger = createLogger('MetaAdsAccountsAPI')
interface MetaAdAccount {
id: string
account_id: string
name: string
account_status: number
}
export async function POST(request: Request) {
try {
const requestId = generateRequestId()
const body = await request.json()
const { credential, workflowId } = body
if (!credential) {
logger.error('Missing credential in request')
return NextResponse.json({ error: 'Credential is required' }, { status: 400 })
}
const authz = await authorizeCredentialUse(request as any, {
credentialId: credential,
workflowId,
})
if (!authz.ok || !authz.credentialOwnerUserId) {
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
}
const accessToken = await refreshAccessTokenIfNeeded(
credential,
authz.credentialOwnerUserId,
requestId
)
if (!accessToken) {
logger.error('Failed to get access token', {
credentialId: credential,
userId: authz.credentialOwnerUserId,
})
return NextResponse.json(
{ error: 'Could not retrieve access token', authRequired: true },
{ status: 401 }
)
}
const url = `${getMetaApiBaseUrl()}/me/adaccounts?fields=account_id,name,account_status&limit=200`
const response = await fetch(url, {
method: 'GET',
headers: { Authorization: `Bearer ${accessToken}` },
})
if (!response.ok) {
const errorData = await response.json()
const errorMessage = errorData?.error?.message ?? 'Failed to fetch ad accounts'
logger.error('Meta API error', { status: response.status, error: errorMessage })
return NextResponse.json({ error: errorMessage }, { status: response.status })
}
const data = await response.json()
const items: MetaAdAccount[] = data.data ?? []
const META_ACCOUNT_STATUS: Record<number, string> = {
1: 'Active',
2: 'Disabled',
3: 'Unsettled',
7: 'Pending Risk Review',
8: 'Pending Settlement',
9: 'In Grace Period',
100: 'Pending Closure',
101: 'Closed',
201: 'Any Active',
202: 'Any Closed',
}
const accounts = items.map((account) => {
const statusLabel = META_ACCOUNT_STATUS[account.account_status] ?? 'Unknown'
const suffix = account.account_status !== 1 ? ` (${statusLabel})` : ''
return {
id: account.account_id,
name: `${account.name || `Account ${account.account_id}`}${suffix}`,
}
})
logger.info(`Successfully fetched ${accounts.length} Meta ad accounts`, {
total: items.length,
active: accounts.length,
})
return NextResponse.json({ accounts })
} catch (error) {
logger.error('Error processing Meta ad accounts request:', error)
return NextResponse.json(
{ error: 'Failed to retrieve ad accounts', details: (error as Error).message },
{ status: 500 }
)
}
}

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, stripActPrefix } from '@/tools/meta_ads/types'
export const dynamic = 'force-dynamic'
const logger = createLogger('MetaAdsCampaignsAPI')
interface MetaCampaign {
id: string
name: string
status: string
}
export async function POST(request: Request) {
try {
const requestId = generateRequestId()
const body = await request.json()
const { credential, workflowId, accountId } = body
if (!credential) {
logger.error('Missing credential in request')
return NextResponse.json({ error: 'Credential is required' }, { status: 400 })
}
if (!accountId) {
logger.error('Missing accountId in request')
return NextResponse.json({ error: 'Account ID is required' }, { status: 400 })
}
const authz = await authorizeCredentialUse(request as any, {
credentialId: credential,
workflowId,
})
if (!authz.ok || !authz.credentialOwnerUserId) {
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
}
const accessToken = await refreshAccessTokenIfNeeded(
credential,
authz.credentialOwnerUserId,
requestId
)
if (!accessToken) {
logger.error('Failed to get access token', {
credentialId: credential,
userId: authz.credentialOwnerUserId,
})
return NextResponse.json(
{ error: 'Could not retrieve access token', authRequired: true },
{ status: 401 }
)
}
const trimmedId = stripActPrefix(String(accountId))
const url = `${getMetaApiBaseUrl()}/act_${trimmedId}/campaigns?fields=id,name,status&limit=200`
const response = await fetch(url, {
method: 'GET',
headers: { Authorization: `Bearer ${accessToken}` },
})
if (!response.ok) {
const errorData = await response.json()
const errorMessage = errorData?.error?.message ?? 'Failed to fetch campaigns'
logger.error('Meta API error', { status: response.status, error: errorMessage })
return NextResponse.json({ error: errorMessage }, { status: response.status })
}
const data = await response.json()
const items: MetaCampaign[] = data.data ?? []
const campaigns = items.map((campaign) => ({
id: campaign.id,
name: campaign.name || `Campaign ${campaign.id}`,
status: campaign.status,
}))
logger.info(`Successfully fetched ${campaigns.length} Meta campaigns`, {
accountId: trimmedId,
total: campaigns.length,
})
return NextResponse.json({ campaigns })
} catch (error) {
logger.error('Error processing Meta campaigns request:', error)
return NextResponse.json(
{ error: 'Failed to retrieve campaigns', details: (error as Error).message },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,268 @@
import { MetaAdsIcon } from '@/components/icons'
import { getScopesForService } from '@/lib/oauth/utils'
import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
export const MetaAdsBlock: BlockConfig = {
type: 'meta_ads',
name: 'Meta Ads',
description: 'Query campaigns, ad sets, ads, and performance insights',
longDescription:
'Connect to Meta Ads to view account info, list campaigns, ad sets, and ads, and get performance insights and metrics.',
docsLink: 'https://docs.sim.ai/tools/meta_ads',
category: 'tools',
bgColor: '#1877F2',
icon: MetaAdsIcon,
authMode: AuthMode.OAuth,
subBlocks: [
{
id: 'operation',
title: 'Operation',
type: 'dropdown',
options: [
{ label: 'Get Account Info', id: 'get_account' },
{ label: 'List Campaigns', id: 'list_campaigns' },
{ label: 'List Ad Sets', id: 'list_ad_sets' },
{ label: 'List Ads', id: 'list_ads' },
{ label: 'Get Insights', id: 'get_insights' },
],
value: () => 'list_campaigns',
},
{
id: 'credential',
title: 'Meta Ads Account',
type: 'oauth-input',
canonicalParamId: 'oauthCredential',
mode: 'basic',
required: true,
serviceId: 'meta-ads',
requiredScopes: getScopesForService('meta-ads'),
placeholder: 'Select Meta Ads account',
},
{
id: 'manualCredential',
title: 'Meta Ads Account',
type: 'short-input',
canonicalParamId: 'oauthCredential',
mode: 'advanced',
placeholder: 'Enter credential ID',
required: true,
},
{
id: 'accountSelector',
title: 'Ad Account',
type: 'project-selector',
canonicalParamId: 'accountId',
serviceId: 'meta-ads',
selectorKey: 'meta-ads.accounts',
placeholder: 'Select ad account',
dependsOn: { any: ['credential', 'manualCredential'] },
mode: 'basic',
required: true,
},
{
id: 'manualAccountId',
title: 'Account ID',
type: 'short-input',
canonicalParamId: 'accountId',
placeholder: 'Meta Ads account ID (numeric, without act_ prefix)',
mode: 'advanced',
required: true,
},
{
id: 'campaignSelector',
title: 'Campaign',
type: 'project-selector',
canonicalParamId: 'campaignId',
serviceId: 'meta-ads',
selectorKey: 'meta-ads.campaigns',
placeholder: 'Select campaign',
dependsOn: { all: ['credential'], any: ['accountSelector', 'manualAccountId'] },
mode: 'basic',
condition: {
field: 'operation',
value: ['list_ad_sets', 'list_ads', 'get_insights'],
},
},
{
id: 'manualCampaignId',
title: 'Campaign ID',
type: 'short-input',
canonicalParamId: 'campaignId',
placeholder: 'Campaign ID to filter by',
mode: 'advanced',
condition: {
field: 'operation',
value: ['list_ad_sets', 'list_ads', 'get_insights'],
},
},
{
id: 'adSetId',
title: 'Ad Set ID',
type: 'short-input',
placeholder: 'Ad set ID to filter by',
condition: {
field: 'operation',
value: ['list_ads', 'get_insights'],
},
},
{
id: 'level',
title: 'Insights Level',
type: 'dropdown',
options: [
{ label: 'Account', id: 'account' },
{ label: 'Campaign', id: 'campaign' },
{ label: 'Ad Set', id: 'adset' },
{ label: 'Ad', id: 'ad' },
],
condition: { field: 'operation', value: 'get_insights' },
required: { field: 'operation', value: 'get_insights' },
value: () => 'campaign',
},
{
id: 'datePreset',
title: 'Date Range',
type: 'dropdown',
options: [
{ label: 'Last 30 Days', id: 'last_30d' },
{ label: 'Last 7 Days', id: 'last_7d' },
{ label: 'Last 14 Days', id: 'last_14d' },
{ label: 'Last 28 Days', id: 'last_28d' },
{ label: 'Last 90 Days', id: 'last_90d' },
{ label: 'Maximum', id: 'maximum' },
{ label: 'Today', id: 'today' },
{ label: 'Yesterday', id: 'yesterday' },
{ label: 'This Month', id: 'this_month' },
{ label: 'Last Month', id: 'last_month' },
{ label: 'Custom', id: 'CUSTOM' },
],
condition: { field: 'operation', value: 'get_insights' },
value: () => 'last_30d',
},
{
id: 'startDate',
title: 'Start Date',
type: 'short-input',
placeholder: 'YYYY-MM-DD',
condition: { field: 'datePreset', value: 'CUSTOM' },
required: { field: 'datePreset', value: 'CUSTOM' },
wandConfig: {
enabled: true,
prompt:
'Generate a date in YYYY-MM-DD format. Return ONLY the date string - no explanations, no extra text.',
generationType: 'timestamp',
},
},
{
id: 'endDate',
title: 'End Date',
type: 'short-input',
placeholder: 'YYYY-MM-DD',
condition: { field: 'datePreset', value: 'CUSTOM' },
required: { field: 'datePreset', value: 'CUSTOM' },
wandConfig: {
enabled: true,
prompt:
'Generate a date in YYYY-MM-DD format. Return ONLY the date string - no explanations, no extra text.',
generationType: 'timestamp',
},
},
{
id: 'status',
title: 'Status Filter',
type: 'dropdown',
options: [
{ label: 'All', id: '' },
{ label: 'Active', id: 'ACTIVE' },
{ label: 'Paused', id: 'PAUSED' },
{ label: 'Archived', id: 'ARCHIVED' },
{ label: 'Deleted', id: 'DELETED' },
],
mode: 'advanced',
condition: {
field: 'operation',
value: ['list_campaigns', 'list_ad_sets', 'list_ads'],
},
},
{
id: 'limit',
title: 'Limit',
type: 'short-input',
placeholder: 'Maximum results to return',
mode: 'advanced',
condition: {
field: 'operation',
value: ['list_campaigns', 'list_ad_sets', 'list_ads', 'get_insights'],
},
},
],
tools: {
access: [
'meta_ads_get_account',
'meta_ads_list_campaigns',
'meta_ads_list_ad_sets',
'meta_ads_list_ads',
'meta_ads_get_insights',
],
config: {
tool: (params) => `meta_ads_${params.operation}`,
params: (params) => {
const { oauthCredential, datePreset, limit, ...rest } = params
const result: Record<string, unknown> = {
...rest,
oauthCredential,
}
if (datePreset && datePreset !== 'CUSTOM') {
result.datePreset = datePreset
}
if (limit !== undefined && limit !== '') {
result.limit = Number(limit)
}
return result
},
},
},
inputs: {
operation: { type: 'string', description: 'Operation to perform' },
oauthCredential: { type: 'string', description: 'Meta Ads OAuth credential' },
accountId: { type: 'string', description: 'Meta Ads account ID' },
campaignId: { type: 'string', description: 'Campaign ID to filter by' },
adSetId: { type: 'string', description: 'Ad set ID to filter by' },
level: { type: 'string', description: 'Insights aggregation level' },
datePreset: { type: 'string', description: 'Date range for insights' },
startDate: { type: 'string', description: 'Custom start date (YYYY-MM-DD)' },
endDate: { type: 'string', description: 'Custom end date (YYYY-MM-DD)' },
status: { type: 'string', description: 'Status filter' },
limit: { type: 'number', description: 'Maximum results to return' },
},
outputs: {
id: { type: 'string', description: 'Account ID (get_account)' },
name: { type: 'string', description: 'Account name (get_account)' },
accountStatus: { type: 'number', description: 'Account status code (get_account)' },
currency: { type: 'string', description: 'Account currency (get_account)' },
timezone: { type: 'string', description: 'Account timezone (get_account)' },
amountSpent: { type: 'string', description: 'Total amount spent (get_account)' },
spendCap: { type: 'string', description: 'Spending limit (get_account)' },
businessCountryCode: { type: 'string', description: 'Country code (get_account)' },
campaigns: { type: 'json', description: 'Campaign data (list_campaigns)' },
adSets: { type: 'json', description: 'Ad set data (list_ad_sets)' },
ads: { type: 'json', description: 'Ad data (list_ads)' },
insights: { type: 'json', description: 'Performance insights (get_insights)' },
totalCount: { type: 'number', description: 'Total number of results' },
},
}

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,55 @@ export function LumaIcon(props: SVGProps<SVGSVGElement>) {
)
}
export function MetaAdsIcon(props: SVGProps<SVGSVGElement>) {
const id = useId()
const lg1 = `${id}-meta-lg1`
const lg2 = `${id}-meta-lg2`
return (
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 287.56 191'>
<defs>
<linearGradient
id={lg1}
x1='62.34'
y1='101.45'
x2='260.34'
y2='91.45'
gradientTransform='matrix(1, 0, 0, -1, 0, 192)'
gradientUnits='userSpaceOnUse'
>
<stop offset='0' stopColor='#0064e1' />
<stop offset='0.4' stopColor='#0064e1' />
<stop offset='0.83' stopColor='#0073ee' />
<stop offset='1' stopColor='#0082fb' />
</linearGradient>
<linearGradient
id={lg2}
x1='41.42'
y1='53'
x2='41.42'
y2='126'
gradientTransform='matrix(1, 0, 0, -1, 0, 192)'
gradientUnits='userSpaceOnUse'
>
<stop offset='0' stopColor='#0082fb' />
<stop offset='1' stopColor='#0064e0' />
</linearGradient>
</defs>
<path
fill='#0081fb'
d='M31.06,126c0,11,2.41,19.41,5.56,24.51A19,19,0,0,0,53.19,160c8.1,0,15.51-2,29.79-21.76,11.44-15.83,24.92-38,34-52l15.36-23.6c10.67-16.39,23-34.61,37.18-47C181.07,5.6,193.54,0,206.09,0c21.07,0,41.14,12.21,56.5,35.11,16.81,25.08,25,56.67,25,89.27,0,19.38-3.82,33.62-10.32,44.87C271,180.13,258.72,191,238.13,191V160c17.63,0,22-16.2,22-34.74,0-26.42-6.16-55.74-19.73-76.69-9.63-14.86-22.11-23.94-35.84-23.94-14.85,0-26.8,11.2-40.23,31.17-7.14,10.61-14.47,23.54-22.7,38.13l-9.06,16c-18.2,32.27-22.81,39.62-31.91,51.75C84.74,183,71.12,191,53.19,191c-21.27,0-34.72-9.21-43-23.09C3.34,156.6,0,141.76,0,124.85Z'
/>
<path
fill={`url(#${lg1})`}
d='M24.49,37.3C38.73,15.35,59.28,0,82.85,0c13.65,0,27.22,4,41.39,15.61,15.5,12.65,32,33.48,52.63,67.81l7.39,12.32c17.84,29.72,28,45,33.93,52.22,7.64,9.26,13,12,19.94,12,17.63,0,22-16.2,22-34.74l27.4-.86c0,19.38-3.82,33.62-10.32,44.87C271,180.13,258.72,191,238.13,191c-12.8,0-24.14-2.78-36.68-14.61-9.64-9.08-20.91-25.21-29.58-39.71L146.08,93.6c-12.94-21.62-24.81-37.74-31.68-45C107,40.71,97.51,31.23,82.35,31.23c-12.27,0-22.69,8.61-31.41,21.78Z'
/>
<path
fill={`url(#${lg2})`}
d='M82.35,31.23c-12.27,0-22.69,8.61-31.41,21.78C38.61,71.62,31.06,99.34,31.06,126c0,11,2.41,19.41,5.56,24.51L10.14,167.91C3.34,156.6,0,141.76,0,124.85,0,94.1,8.44,62.05,24.49,37.3,38.73,15.35,59.28,0,82.85,0Z'
/>
</svg>
)
}
export function MailchimpIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg

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

@@ -323,6 +323,39 @@ export const auth = betterAuth({
}
}
if (account.providerId === 'meta-ads' && account.accessToken) {
try {
const exchangeUrl = new URL('https://graph.facebook.com/v24.0/oauth/access_token')
exchangeUrl.searchParams.set('grant_type', 'fb_exchange_token')
exchangeUrl.searchParams.set('client_id', env.META_ADS_CLIENT_ID as string)
exchangeUrl.searchParams.set('client_secret', env.META_ADS_CLIENT_SECRET as string)
exchangeUrl.searchParams.set('fb_exchange_token', account.accessToken)
const exchangeResponse = await fetch(exchangeUrl.toString())
if (exchangeResponse.ok) {
const exchangeData = await exchangeResponse.json()
const longLivedToken = exchangeData.access_token
const expiresIn = exchangeData.expires_in ?? 5_184_000
await db
.update(schema.account)
.set({
accessToken: longLivedToken,
accessTokenExpiresAt: new Date(Date.now() + expiresIn * 1000),
})
.where(eq(schema.account.id, account.id))
logger.info('Exchanged Meta short-lived token for long-lived token', { expiresIn })
} else {
logger.warn('Failed to exchange Meta token for long-lived token', {
status: exchangeResponse.status,
})
}
} catch (error) {
logger.error('Error exchanging Meta token', { error })
}
}
if (isMicrosoftProvider(account.providerId)) {
await db
.update(schema.account)
@@ -474,6 +507,7 @@ export const auth = betterAuth({
'shopify',
'trello',
'calcom',
'meta-ads',
...SSO_TRUSTED_PROVIDERS,
],
},
@@ -2418,6 +2452,55 @@ export const auth = betterAuth({
},
},
// Meta Ads provider
{
providerId: 'meta-ads',
clientId: env.META_ADS_CLIENT_ID as string,
clientSecret: env.META_ADS_CLIENT_SECRET as string,
authorizationUrl: 'https://www.facebook.com/v24.0/dialog/oauth',
tokenUrl: 'https://graph.facebook.com/v24.0/oauth/access_token',
userInfoUrl: 'https://graph.facebook.com/v24.0/me',
scopes: getCanonicalScopesForProvider('meta-ads'),
responseType: 'code',
prompt: 'consent',
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/meta-ads`,
getUserInfo: async (tokens) => {
try {
logger.info('Fetching Meta user profile')
const response = await fetch(
'https://graph.facebook.com/v24.0/me?fields=id,name,email',
{
headers: { Authorization: `Bearer ${tokens.accessToken}` },
}
)
if (!response.ok) {
await response.text().catch(() => {})
logger.error('Failed to fetch Meta user info', {
status: response.status,
statusText: response.statusText,
})
throw new Error('Failed to fetch user info')
}
const profile = await response.json()
return {
id: `${profile.id}-${crypto.randomUUID()}`,
name: profile.name || 'Meta User',
email: profile.email || `${profile.id}@facebook.user`,
emailVerified: true,
createdAt: new Date(),
updatedAt: new Date(),
}
} catch (error) {
logger.error('Error in Meta getUserInfo:', { error })
return null
}
},
},
// Zoom provider
{
providerId: 'zoom',

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, stripActPrefix } from '@/tools/meta_ads/types'
import type { ToolConfig } from '@/tools/types'
export const metaAdsGetAccountTool: ToolConfig<MetaAdsGetAccountParams, MetaAdsGetAccountResponse> =
{
id: 'meta_ads_get_account',
name: 'Get Meta Ads Account',
description: 'Get information about a Meta Ads account',
version: '1.0.0',
oauth: {
required: true,
provider: 'meta-ads',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token for the Meta Marketing API',
},
accountId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Meta Ads account ID (numeric, without act_ prefix)',
},
},
request: {
url: (params) => {
const fields =
'id,name,account_status,currency,timezone_name,amount_spent,spend_cap,business_country_code'
return `${getMetaApiBaseUrl()}/act_${stripActPrefix(params.accountId)}?fields=${fields}`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
const errorMessage = data?.error?.message ?? 'Unknown error'
return {
success: false,
output: {
id: '',
name: '',
accountStatus: 0,
currency: '',
timezone: '',
amountSpent: '0',
spendCap: null,
businessCountryCode: null,
},
error: errorMessage,
}
}
return {
success: true,
output: {
id: data.id ?? '',
name: data.name ?? '',
accountStatus: data.account_status ?? 0,
currency: data.currency ?? '',
timezone: data.timezone_name ?? '',
amountSpent: data.amount_spent ?? '0',
spendCap: data.spend_cap ?? null,
businessCountryCode: data.business_country_code ?? null,
},
}
},
outputs: {
id: { type: 'string', description: 'Ad account ID' },
name: { type: 'string', description: 'Ad account name' },
accountStatus: { type: 'number', description: 'Account status code' },
currency: { type: 'string', description: 'Account currency (e.g., USD)' },
timezone: { type: 'string', description: 'Account timezone' },
amountSpent: { type: 'string', description: 'Total amount spent' },
spendCap: {
type: 'string',
description: 'Spending limit for the account',
optional: true,
},
businessCountryCode: {
type: 'string',
description: 'Country code for the business',
optional: true,
},
},
}

View File

@@ -0,0 +1,214 @@
import type { MetaAdsGetInsightsParams, MetaAdsGetInsightsResponse } from '@/tools/meta_ads/types'
import { getMetaApiBaseUrl, stripActPrefix } from '@/tools/meta_ads/types'
import type { ToolConfig } from '@/tools/types'
export const metaAdsGetInsightsTool: ToolConfig<
MetaAdsGetInsightsParams,
MetaAdsGetInsightsResponse
> = {
id: 'meta_ads_get_insights',
name: 'Get Meta Ads Insights',
description: 'Get performance insights and metrics for Meta Ads campaigns, ad sets, or ads',
version: '1.0.0',
oauth: {
required: true,
provider: 'meta-ads',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token for the Meta Marketing API',
},
accountId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Meta Ads account ID (numeric, without act_ prefix)',
},
level: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Aggregation level for insights (account, campaign, adset, ad)',
},
campaignId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter insights by campaign ID',
},
adSetId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter insights by ad set ID',
},
datePreset: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Predefined date range (today, yesterday, last_7d, last_14d, last_28d, last_30d, last_90d, maximum, this_month, last_month)',
},
startDate: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Custom start date in YYYY-MM-DD format',
},
endDate: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Custom end date in YYYY-MM-DD format',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of insight rows to return',
},
},
request: {
url: (params) => {
const fields =
'account_id,campaign_id,campaign_name,adset_id,adset_name,ad_id,ad_name,impressions,clicks,spend,ctr,cpc,cpm,reach,frequency,actions,date_start,date_stop'
const searchParams = new URLSearchParams({ fields, level: params.level })
if (params.startDate && params.endDate) {
searchParams.set(
'time_range',
JSON.stringify({ since: params.startDate, until: params.endDate })
)
} else if (params.datePreset) {
searchParams.set('date_preset', params.datePreset)
} else {
searchParams.set('date_preset', 'last_30d')
}
if (params.limit) {
searchParams.set('limit', String(params.limit))
}
let parentId: string
if (params.adSetId) {
parentId = params.adSetId.trim()
} else if (params.campaignId) {
parentId = params.campaignId.trim()
} else {
parentId = `act_${stripActPrefix(params.accountId)}`
}
return `${getMetaApiBaseUrl()}/${parentId}/insights?${searchParams.toString()}`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
const errorMessage = data?.error?.message ?? 'Unknown error'
return {
success: false,
output: { insights: [], totalCount: 0 },
error: errorMessage,
}
}
const items = data.data ?? []
const insights = items.map((i: Record<string, unknown>) => {
const actions = (i.actions as Array<Record<string, unknown>>) ?? []
const conversionTypes = new Set([
'offsite_conversion',
'onsite_conversion',
'app_custom_event',
])
const conversionPrefixes = ['offsite_conversion.', 'onsite_conversion.', 'app_custom_event.']
const conversions = actions
.filter((a) => {
const actionType = a.action_type as string
return (
conversionTypes.has(actionType) ||
conversionPrefixes.some((prefix) => actionType?.startsWith(prefix))
)
})
.reduce((sum, a) => sum + Number(a.value ?? 0), 0)
return {
accountId: (i.account_id as string) ?? null,
campaignId: (i.campaign_id as string) ?? null,
campaignName: (i.campaign_name as string) ?? null,
adSetId: (i.adset_id as string) ?? null,
adSetName: (i.adset_name as string) ?? null,
adId: (i.ad_id as string) ?? null,
adName: (i.ad_name as string) ?? null,
impressions: (i.impressions as string) ?? '0',
clicks: (i.clicks as string) ?? '0',
spend: (i.spend as string) ?? '0',
ctr: (i.ctr as string) ?? null,
cpc: (i.cpc as string) ?? null,
cpm: (i.cpm as string) ?? null,
reach: (i.reach as string) ?? null,
frequency: (i.frequency as string) ?? null,
conversions,
dateStart: (i.date_start as string) ?? '',
dateStop: (i.date_stop as string) ?? '',
}
})
return {
success: true,
output: {
insights,
totalCount: insights.length,
},
}
},
outputs: {
insights: {
type: 'array',
description: 'Performance insight rows',
items: {
type: 'object',
properties: {
accountId: { type: 'string', description: 'Ad account ID' },
campaignId: { type: 'string', description: 'Campaign ID' },
campaignName: { type: 'string', description: 'Campaign name' },
adSetId: { type: 'string', description: 'Ad set ID' },
adSetName: { type: 'string', description: 'Ad set name' },
adId: { type: 'string', description: 'Ad ID' },
adName: { type: 'string', description: 'Ad name' },
impressions: { type: 'string', description: 'Number of impressions' },
clicks: { type: 'string', description: 'Number of clicks' },
spend: { type: 'string', description: 'Amount spent in account currency' },
ctr: { type: 'string', description: 'Click-through rate' },
cpc: { type: 'string', description: 'Cost per click' },
cpm: { type: 'string', description: 'Cost per 1,000 impressions' },
reach: { type: 'string', description: 'Number of unique users reached' },
frequency: {
type: 'string',
description: 'Average number of times each person saw the ad',
},
conversions: { type: 'number', description: 'Total conversions from actions' },
dateStart: { type: 'string', description: 'Start date of the reporting period' },
dateStop: { type: 'string', description: 'End date of the reporting period' },
},
},
},
totalCount: {
type: 'number',
description:
'Number of insight rows returned in this response (may be limited by pagination)',
},
},
}

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, stripActPrefix } from '@/tools/meta_ads/types'
import type { ToolConfig } from '@/tools/types'
export const metaAdsListAdSetsTool: ToolConfig<MetaAdsListAdSetsParams, MetaAdsListAdSetsResponse> =
{
id: 'meta_ads_list_ad_sets',
name: 'List Meta Ads Ad Sets',
description: 'List ad sets in a Meta Ads account or campaign',
version: '1.0.0',
oauth: {
required: true,
provider: 'meta-ads',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token for the Meta Marketing API',
},
accountId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Meta Ads account ID (numeric, without act_ prefix)',
},
campaignId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter ad sets by campaign ID',
},
status: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter by ad set status (ACTIVE, PAUSED, ARCHIVED, DELETED)',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of ad sets to return',
},
},
request: {
url: (params) => {
const fields = 'id,name,status,campaign_id,daily_budget,lifetime_budget,start_time,end_time'
const searchParams = new URLSearchParams({ fields })
if (params.status) {
searchParams.set('effective_status', JSON.stringify([params.status]))
}
if (params.limit) {
searchParams.set('limit', String(params.limit))
}
const parentId = params.campaignId?.trim() || `act_${stripActPrefix(params.accountId)}`
return `${getMetaApiBaseUrl()}/${parentId}/adsets?${searchParams.toString()}`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
const errorMessage = data?.error?.message ?? 'Unknown error'
return {
success: false,
output: { adSets: [], totalCount: 0 },
error: errorMessage,
}
}
const items = data.data ?? []
const adSets = items.map((a: Record<string, unknown>) => ({
id: (a.id as string) ?? '',
name: (a.name as string) ?? '',
status: (a.status as string) ?? '',
campaignId: (a.campaign_id as string) ?? '',
dailyBudget: (a.daily_budget as string) ?? null,
lifetimeBudget: (a.lifetime_budget as string) ?? null,
startTime: (a.start_time as string) ?? null,
endTime: (a.end_time as string) ?? null,
}))
return {
success: true,
output: {
adSets,
totalCount: adSets.length,
},
}
},
outputs: {
adSets: {
type: 'array',
description: 'List of ad sets',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Ad set ID' },
name: { type: 'string', description: 'Ad set name' },
status: { type: 'string', description: 'Ad set status' },
campaignId: { type: 'string', description: 'Parent campaign ID' },
dailyBudget: { type: 'string', description: 'Daily budget in account currency cents' },
lifetimeBudget: {
type: 'string',
description: 'Lifetime budget in account currency cents',
},
startTime: { type: 'string', description: 'Ad set start time (ISO 8601)' },
endTime: { type: 'string', description: 'Ad set end time (ISO 8601)' },
},
},
},
totalCount: {
type: 'number',
description: 'Number of ad sets returned in this response (may be limited by pagination)',
},
},
}

View File

@@ -0,0 +1,138 @@
import type { MetaAdsListAdsParams, MetaAdsListAdsResponse } from '@/tools/meta_ads/types'
import { getMetaApiBaseUrl, stripActPrefix } from '@/tools/meta_ads/types'
import type { ToolConfig } from '@/tools/types'
export const metaAdsListAdsTool: ToolConfig<MetaAdsListAdsParams, MetaAdsListAdsResponse> = {
id: 'meta_ads_list_ads',
name: 'List Meta Ads',
description: 'List ads in a Meta Ads account, campaign, or ad set',
version: '1.0.0',
oauth: {
required: true,
provider: 'meta-ads',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token for the Meta Marketing API',
},
accountId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Meta Ads account ID (numeric, without act_ prefix)',
},
campaignId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter ads by campaign ID',
},
adSetId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter ads by ad set ID',
},
status: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter by ad status (ACTIVE, PAUSED, ARCHIVED, DELETED)',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of ads to return',
},
},
request: {
url: (params) => {
const fields = 'id,name,status,adset_id,campaign_id,created_time,updated_time'
const searchParams = new URLSearchParams({ fields })
if (params.status) {
searchParams.set('effective_status', JSON.stringify([params.status]))
}
if (params.limit) {
searchParams.set('limit', String(params.limit))
}
let parentId: string
if (params.adSetId) {
parentId = params.adSetId.trim()
} else if (params.campaignId) {
parentId = params.campaignId.trim()
} else {
parentId = `act_${stripActPrefix(params.accountId)}`
}
return `${getMetaApiBaseUrl()}/${parentId}/ads?${searchParams.toString()}`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
const errorMessage = data?.error?.message ?? 'Unknown error'
return {
success: false,
output: { ads: [], totalCount: 0 },
error: errorMessage,
}
}
const items = data.data ?? []
const ads = items.map((a: Record<string, unknown>) => ({
id: (a.id as string) ?? '',
name: (a.name as string) ?? '',
status: (a.status as string) ?? '',
adSetId: (a.adset_id as string) ?? null,
campaignId: (a.campaign_id as string) ?? null,
createdTime: (a.created_time as string) ?? null,
updatedTime: (a.updated_time as string) ?? null,
}))
return {
success: true,
output: {
ads,
totalCount: ads.length,
},
}
},
outputs: {
ads: {
type: 'array',
description: 'List of ads',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Ad ID' },
name: { type: 'string', description: 'Ad name' },
status: { type: 'string', description: 'Ad status' },
adSetId: { type: 'string', description: 'Parent ad set ID' },
campaignId: { type: 'string', description: 'Parent campaign ID' },
createdTime: { type: 'string', description: 'Ad creation time (ISO 8601)' },
updatedTime: { type: 'string', description: 'Ad last update time (ISO 8601)' },
},
},
},
totalCount: {
type: 'number',
description: 'Number of ads returned in this response (may be limited by pagination)',
},
},
}

View File

@@ -0,0 +1,129 @@
import type {
MetaAdsListCampaignsParams,
MetaAdsListCampaignsResponse,
} from '@/tools/meta_ads/types'
import { getMetaApiBaseUrl, stripActPrefix } from '@/tools/meta_ads/types'
import type { ToolConfig } from '@/tools/types'
export const metaAdsListCampaignsTool: ToolConfig<
MetaAdsListCampaignsParams,
MetaAdsListCampaignsResponse
> = {
id: 'meta_ads_list_campaigns',
name: 'List Meta Ads Campaigns',
description: 'List campaigns in a Meta Ads account with optional status filtering',
version: '1.0.0',
oauth: {
required: true,
provider: 'meta-ads',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token for the Meta Marketing API',
},
accountId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Meta Ads account ID (numeric, without act_ prefix)',
},
status: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter by campaign status (ACTIVE, PAUSED, ARCHIVED, DELETED)',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of campaigns to return',
},
},
request: {
url: (params) => {
const fields =
'id,name,status,objective,daily_budget,lifetime_budget,created_time,updated_time'
const searchParams = new URLSearchParams({ fields })
if (params.status) {
searchParams.set('effective_status', JSON.stringify([params.status]))
}
if (params.limit) {
searchParams.set('limit', String(params.limit))
}
return `${getMetaApiBaseUrl()}/act_${stripActPrefix(params.accountId)}/campaigns?${searchParams.toString()}`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
const errorMessage = data?.error?.message ?? 'Unknown error'
return {
success: false,
output: { campaigns: [], totalCount: 0 },
error: errorMessage,
}
}
const items = data.data ?? []
const campaigns = items.map((c: Record<string, unknown>) => ({
id: (c.id as string) ?? '',
name: (c.name as string) ?? '',
status: (c.status as string) ?? '',
objective: (c.objective as string) ?? null,
dailyBudget: (c.daily_budget as string) ?? null,
lifetimeBudget: (c.lifetime_budget as string) ?? null,
createdTime: (c.created_time as string) ?? null,
updatedTime: (c.updated_time as string) ?? null,
}))
return {
success: true,
output: {
campaigns,
totalCount: campaigns.length,
},
}
},
outputs: {
campaigns: {
type: 'array',
description: 'List of campaigns in the account',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Campaign ID' },
name: { type: 'string', description: 'Campaign name' },
status: { type: 'string', description: 'Campaign status (ACTIVE, PAUSED, etc.)' },
objective: { type: 'string', description: 'Campaign objective' },
dailyBudget: { type: 'string', description: 'Daily budget in account currency cents' },
lifetimeBudget: {
type: 'string',
description: 'Lifetime budget in account currency cents',
},
createdTime: { type: 'string', description: 'Campaign creation time (ISO 8601)' },
updatedTime: { type: 'string', description: 'Campaign last update time (ISO 8601)' },
},
},
},
totalCount: {
type: 'number',
description: 'Number of campaigns returned in this response (may be limited by pagination)',
},
},
}

View File

@@ -0,0 +1,147 @@
import type { ToolResponse } from '@/tools/types'
const META_API_VERSION = 'v24.0'
export function getMetaApiBaseUrl(): string {
return `https://graph.facebook.com/${META_API_VERSION}`
}
/**
* Strips the `act_` prefix from an account ID if present, so that
* callers can safely wrap the result with `act_` without double-prefixing.
*/
export function stripActPrefix(accountId: string): string {
const trimmed = accountId.trim()
return trimmed.startsWith('act_') ? trimmed.slice(4) : trimmed
}
export interface MetaAdsBaseParams {
accessToken: string
accountId: string
}
export interface MetaAdsGetAccountParams extends MetaAdsBaseParams {}
export interface MetaAdsListCampaignsParams extends MetaAdsBaseParams {
status?: string
limit?: number
}
export interface MetaAdsListAdSetsParams extends MetaAdsBaseParams {
campaignId?: string
status?: string
limit?: number
}
export interface MetaAdsListAdsParams extends MetaAdsBaseParams {
campaignId?: string
adSetId?: string
status?: string
limit?: number
}
export interface MetaAdsGetInsightsParams extends MetaAdsBaseParams {
level: string
campaignId?: string
adSetId?: string
datePreset?: string
startDate?: string
endDate?: string
limit?: number
}
export interface MetaAdsAccount {
id: string
name: string
accountStatus: number
currency: string
timezone: string
amountSpent: string
spendCap: string | null
businessCountryCode: string | null
}
export interface MetaAdsGetAccountResponse extends ToolResponse {
output: MetaAdsAccount
}
export interface MetaAdsCampaign {
id: string
name: string
status: string
objective: string | null
dailyBudget: string | null
lifetimeBudget: string | null
createdTime: string | null
updatedTime: string | null
}
export interface MetaAdsListCampaignsResponse extends ToolResponse {
output: {
campaigns: MetaAdsCampaign[]
totalCount: number
}
}
export interface MetaAdsAdSet {
id: string
name: string
status: string
campaignId: string
dailyBudget: string | null
lifetimeBudget: string | null
startTime: string | null
endTime: string | null
}
export interface MetaAdsListAdSetsResponse extends ToolResponse {
output: {
adSets: MetaAdsAdSet[]
totalCount: number
}
}
export interface MetaAdsAd {
id: string
name: string
status: string
adSetId: string | null
campaignId: string | null
createdTime: string | null
updatedTime: string | null
}
export interface MetaAdsListAdsResponse extends ToolResponse {
output: {
ads: MetaAdsAd[]
totalCount: number
}
}
export interface MetaAdsInsight {
accountId: string | null
campaignId: string | null
campaignName: string | null
adSetId: string | null
adSetName: string | null
adId: string | null
adName: string | null
impressions: string
clicks: string
spend: string
ctr: string | null
cpc: string | null
cpm: string | null
reach: string | null
frequency: string | null
conversions: number
dateStart: string
dateStop: string
}
export interface MetaAdsGetInsightsResponse extends ToolResponse {
output: {
insights: MetaAdsInsight[]
totalCount: number
}
}

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,