@slates-integrations/google-ads 0.2.0-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -0
- package/docs/SPEC.md +98 -0
- package/logo.png +0 -0
- package/package.json +21 -0
- package/slate.json +21 -0
- package/src/auth.contract.test.ts +174 -0
- package/src/auth.ts +150 -0
- package/src/config.ts +13 -0
- package/src/index.ts +34 -0
- package/src/lib/client.ts +245 -0
- package/src/lib/helpers.ts +20 -0
- package/src/provider.contract.test.ts +75 -0
- package/src/scopes.ts +22 -0
- package/src/spec.ts +13 -0
- package/src/tools/generate-keyword-ideas.ts +114 -0
- package/src/tools/index.ts +11 -0
- package/src/tools/list-accounts.ts +90 -0
- package/src/tools/manage-ad-groups.ts +125 -0
- package/src/tools/manage-ads.ts +162 -0
- package/src/tools/manage-audience-lists.ts +134 -0
- package/src/tools/manage-bidding-strategies.ts +153 -0
- package/src/tools/manage-campaigns.ts +275 -0
- package/src/tools/manage-conversion-actions.ts +224 -0
- package/src/tools/manage-keywords.ts +167 -0
- package/src/tools/search-reports.ts +71 -0
- package/src/tools/upload-offline-conversions.ts +93 -0
- package/src/triggers/index.ts +1 -0
- package/src/triggers/lead-form-submit.ts +135 -0
- package/tsconfig.json +23 -0
- package/vitest.config.ts +7 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { SlateTool } from 'slates';
|
|
2
|
+
import { googleAdsActionScopes } from '../scopes';
|
|
3
|
+
import { spec } from '../spec';
|
|
4
|
+
import { createClient } from '../lib/helpers';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
|
|
7
|
+
export let manageAdGroups = SlateTool.create(spec, {
|
|
8
|
+
name: 'Manage Ad Groups',
|
|
9
|
+
key: 'manage_ad_groups',
|
|
10
|
+
description: `Create, update, or remove ad groups within a Google Ads campaign. Ad groups organize ads and keywords within a campaign.
|
|
11
|
+
|
|
12
|
+
Supports setting the ad group name, status, type, CPC bid, and targeting URL.`,
|
|
13
|
+
instructions: [
|
|
14
|
+
'CPC bid values use micros (1 currency unit = 1,000,000 micros).',
|
|
15
|
+
'Ad group resource names follow: customers/{customerId}/adGroups/{adGroupId}'
|
|
16
|
+
]
|
|
17
|
+
})
|
|
18
|
+
.scopes(googleAdsActionScopes.manageAdGroups)
|
|
19
|
+
.input(
|
|
20
|
+
z.object({
|
|
21
|
+
customerId: z.string().describe('The Google Ads customer account ID'),
|
|
22
|
+
operation: z.enum(['create', 'update', 'remove']).describe('The operation to perform'),
|
|
23
|
+
adGroupId: z.string().optional().describe('Ad group ID (required for update/remove)'),
|
|
24
|
+
campaignId: z.string().optional().describe('Campaign ID (required for create)'),
|
|
25
|
+
name: z.string().optional().describe('Ad group name'),
|
|
26
|
+
status: z.enum(['ENABLED', 'PAUSED', 'REMOVED']).optional().describe('Ad group status'),
|
|
27
|
+
type: z
|
|
28
|
+
.enum([
|
|
29
|
+
'SEARCH_STANDARD',
|
|
30
|
+
'DISPLAY_STANDARD',
|
|
31
|
+
'SHOPPING_PRODUCT_ADS',
|
|
32
|
+
'VIDEO_BUMPER',
|
|
33
|
+
'VIDEO_TRUE_VIEW_IN_STREAM',
|
|
34
|
+
'VIDEO_NON_SKIPPABLE_IN_STREAM'
|
|
35
|
+
])
|
|
36
|
+
.optional()
|
|
37
|
+
.describe('Ad group type (required for create)'),
|
|
38
|
+
cpcBidMicros: z.string().optional().describe('CPC bid in micros'),
|
|
39
|
+
finalUrls: z.array(z.string()).optional().describe('Final URLs for the ad group')
|
|
40
|
+
})
|
|
41
|
+
)
|
|
42
|
+
.output(
|
|
43
|
+
z.object({
|
|
44
|
+
adGroupResourceName: z
|
|
45
|
+
.string()
|
|
46
|
+
.optional()
|
|
47
|
+
.describe('Resource name of the created/updated ad group'),
|
|
48
|
+
mutateResults: z.any().optional().describe('Raw API response')
|
|
49
|
+
})
|
|
50
|
+
)
|
|
51
|
+
.handleInvocation(async ctx => {
|
|
52
|
+
let client = createClient(ctx.auth, ctx.config);
|
|
53
|
+
let { customerId, operation } = ctx.input;
|
|
54
|
+
let cid = customerId.replace(/-/g, '');
|
|
55
|
+
|
|
56
|
+
if (operation === 'remove') {
|
|
57
|
+
if (!ctx.input.adGroupId) throw new Error('adGroupId is required for remove operation');
|
|
58
|
+
let result = await client.mutateAdGroups(cid, [
|
|
59
|
+
{
|
|
60
|
+
remove: `customers/${cid}/adGroups/${ctx.input.adGroupId}`
|
|
61
|
+
}
|
|
62
|
+
]);
|
|
63
|
+
return {
|
|
64
|
+
output: {
|
|
65
|
+
adGroupResourceName: `customers/${cid}/adGroups/${ctx.input.adGroupId}`,
|
|
66
|
+
mutateResults: result
|
|
67
|
+
},
|
|
68
|
+
message: `Ad group **${ctx.input.adGroupId}** removed.`
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (operation === 'create') {
|
|
73
|
+
if (!ctx.input.campaignId)
|
|
74
|
+
throw new Error('campaignId is required for create operation');
|
|
75
|
+
let adGroupData: Record<string, any> = {
|
|
76
|
+
name: ctx.input.name,
|
|
77
|
+
campaign: `customers/${cid}/campaigns/${ctx.input.campaignId}`,
|
|
78
|
+
status: ctx.input.status || 'ENABLED'
|
|
79
|
+
};
|
|
80
|
+
if (ctx.input.type) adGroupData.type = ctx.input.type;
|
|
81
|
+
if (ctx.input.cpcBidMicros) adGroupData.cpcBidMicros = ctx.input.cpcBidMicros;
|
|
82
|
+
if (ctx.input.finalUrls) adGroupData.finalUrls = ctx.input.finalUrls;
|
|
83
|
+
|
|
84
|
+
let result = await client.mutateAdGroups(cid, [{ create: adGroupData }]);
|
|
85
|
+
return {
|
|
86
|
+
output: {
|
|
87
|
+
adGroupResourceName: result.results?.[0]?.resourceName,
|
|
88
|
+
mutateResults: result
|
|
89
|
+
},
|
|
90
|
+
message: `Ad group **${ctx.input.name}** created.`
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Update
|
|
95
|
+
if (!ctx.input.adGroupId) throw new Error('adGroupId is required for update operation');
|
|
96
|
+
let resourceName = `customers/${cid}/adGroups/${ctx.input.adGroupId}`;
|
|
97
|
+
let updateData: Record<string, any> = { resourceName };
|
|
98
|
+
let maskFields: string[] = [];
|
|
99
|
+
|
|
100
|
+
if (ctx.input.name !== undefined) {
|
|
101
|
+
updateData.name = ctx.input.name;
|
|
102
|
+
maskFields.push('name');
|
|
103
|
+
}
|
|
104
|
+
if (ctx.input.status !== undefined) {
|
|
105
|
+
updateData.status = ctx.input.status;
|
|
106
|
+
maskFields.push('status');
|
|
107
|
+
}
|
|
108
|
+
if (ctx.input.cpcBidMicros !== undefined) {
|
|
109
|
+
updateData.cpcBidMicros = ctx.input.cpcBidMicros;
|
|
110
|
+
maskFields.push('cpcBidMicros');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
let result = await client.mutateAdGroups(cid, [
|
|
114
|
+
{
|
|
115
|
+
update: updateData,
|
|
116
|
+
updateMask: maskFields.join(',')
|
|
117
|
+
}
|
|
118
|
+
]);
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
output: { adGroupResourceName: resourceName, mutateResults: result },
|
|
122
|
+
message: `Ad group **${ctx.input.adGroupId}** updated (fields: ${maskFields.join(', ')}).`
|
|
123
|
+
};
|
|
124
|
+
})
|
|
125
|
+
.build();
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { SlateTool } from 'slates';
|
|
2
|
+
import { googleAdsActionScopes } from '../scopes';
|
|
3
|
+
import { spec } from '../spec';
|
|
4
|
+
import { createClient } from '../lib/helpers';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
|
|
7
|
+
export let manageAds = SlateTool.create(spec, {
|
|
8
|
+
name: 'Manage Ads',
|
|
9
|
+
key: 'manage_ads',
|
|
10
|
+
description: `Create, update, or remove ads within an ad group. Supports responsive search ads, expanded text ads, responsive display ads, and other ad formats.
|
|
11
|
+
|
|
12
|
+
For responsive search ads, provide headlines and descriptions. Google will automatically test combinations. Pin headlines/descriptions to specific positions if needed.`,
|
|
13
|
+
instructions: [
|
|
14
|
+
'Responsive search ads require at least 3 headlines and 2 descriptions.',
|
|
15
|
+
'You can provide up to 15 headlines and 4 descriptions for responsive search ads.',
|
|
16
|
+
'Use pinnedField to pin a headline/description to a specific position (HEADLINE_1, HEADLINE_2, HEADLINE_3, DESCRIPTION_1, DESCRIPTION_2).'
|
|
17
|
+
]
|
|
18
|
+
})
|
|
19
|
+
.scopes(googleAdsActionScopes.manageAds)
|
|
20
|
+
.input(
|
|
21
|
+
z.object({
|
|
22
|
+
customerId: z.string().describe('The Google Ads customer account ID'),
|
|
23
|
+
operation: z.enum(['create', 'update', 'remove']).describe('The operation to perform'),
|
|
24
|
+
adGroupId: z.string().optional().describe('Ad group ID (required for create)'),
|
|
25
|
+
adGroupAdResourceName: z
|
|
26
|
+
.string()
|
|
27
|
+
.optional()
|
|
28
|
+
.describe(
|
|
29
|
+
'Full resource name of the ad group ad (required for update/remove, e.g., customers/{id}/adGroupAds/{adGroupId}~{adId})'
|
|
30
|
+
),
|
|
31
|
+
status: z.enum(['ENABLED', 'PAUSED', 'REMOVED']).optional().describe('Ad status'),
|
|
32
|
+
adType: z
|
|
33
|
+
.enum(['RESPONSIVE_SEARCH_AD', 'RESPONSIVE_DISPLAY_AD', 'EXPANDED_TEXT_AD'])
|
|
34
|
+
.optional()
|
|
35
|
+
.describe('Type of ad to create'),
|
|
36
|
+
responsiveSearchAd: z
|
|
37
|
+
.object({
|
|
38
|
+
headlines: z
|
|
39
|
+
.array(
|
|
40
|
+
z.object({
|
|
41
|
+
text: z.string().describe('Headline text (max 30 chars)'),
|
|
42
|
+
pinnedField: z.string().optional().describe('Pin position, e.g., HEADLINE_1')
|
|
43
|
+
})
|
|
44
|
+
)
|
|
45
|
+
.describe('List of headlines'),
|
|
46
|
+
descriptions: z
|
|
47
|
+
.array(
|
|
48
|
+
z.object({
|
|
49
|
+
text: z.string().describe('Description text (max 90 chars)'),
|
|
50
|
+
pinnedField: z
|
|
51
|
+
.string()
|
|
52
|
+
.optional()
|
|
53
|
+
.describe('Pin position, e.g., DESCRIPTION_1')
|
|
54
|
+
})
|
|
55
|
+
)
|
|
56
|
+
.describe('List of descriptions'),
|
|
57
|
+
path1: z.string().optional().describe('First part of display URL path'),
|
|
58
|
+
path2: z.string().optional().describe('Second part of display URL path')
|
|
59
|
+
})
|
|
60
|
+
.optional()
|
|
61
|
+
.describe('Responsive search ad content'),
|
|
62
|
+
finalUrls: z.array(z.string()).optional().describe('Landing page URLs'),
|
|
63
|
+
finalMobileUrls: z.array(z.string()).optional().describe('Mobile landing page URLs'),
|
|
64
|
+
trackingUrlTemplate: z.string().optional().describe('Tracking URL template')
|
|
65
|
+
})
|
|
66
|
+
)
|
|
67
|
+
.output(
|
|
68
|
+
z.object({
|
|
69
|
+
adGroupAdResourceName: z
|
|
70
|
+
.string()
|
|
71
|
+
.optional()
|
|
72
|
+
.describe('Resource name of the ad group ad'),
|
|
73
|
+
mutateResults: z.any().optional().describe('Raw API response')
|
|
74
|
+
})
|
|
75
|
+
)
|
|
76
|
+
.handleInvocation(async ctx => {
|
|
77
|
+
let client = createClient(ctx.auth, ctx.config);
|
|
78
|
+
let { customerId, operation } = ctx.input;
|
|
79
|
+
let cid = customerId.replace(/-/g, '');
|
|
80
|
+
|
|
81
|
+
if (operation === 'remove') {
|
|
82
|
+
if (!ctx.input.adGroupAdResourceName)
|
|
83
|
+
throw new Error('adGroupAdResourceName is required for remove');
|
|
84
|
+
let result = await client.mutateAdGroupAds(cid, [
|
|
85
|
+
{ remove: ctx.input.adGroupAdResourceName }
|
|
86
|
+
]);
|
|
87
|
+
return {
|
|
88
|
+
output: {
|
|
89
|
+
adGroupAdResourceName: ctx.input.adGroupAdResourceName,
|
|
90
|
+
mutateResults: result
|
|
91
|
+
},
|
|
92
|
+
message: `Ad removed successfully.`
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (operation === 'create') {
|
|
97
|
+
if (!ctx.input.adGroupId) throw new Error('adGroupId is required for create');
|
|
98
|
+
|
|
99
|
+
let adData: Record<string, any> = {
|
|
100
|
+
adGroup: `customers/${cid}/adGroups/${ctx.input.adGroupId}`,
|
|
101
|
+
status: ctx.input.status || 'ENABLED',
|
|
102
|
+
ad: {} as Record<string, any>
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
if (ctx.input.finalUrls) adData.ad.finalUrls = ctx.input.finalUrls;
|
|
106
|
+
if (ctx.input.finalMobileUrls) adData.ad.finalMobileUrls = ctx.input.finalMobileUrls;
|
|
107
|
+
if (ctx.input.trackingUrlTemplate)
|
|
108
|
+
adData.ad.trackingUrlTemplate = ctx.input.trackingUrlTemplate;
|
|
109
|
+
|
|
110
|
+
if (ctx.input.adType === 'RESPONSIVE_SEARCH_AD' && ctx.input.responsiveSearchAd) {
|
|
111
|
+
let rsa = ctx.input.responsiveSearchAd;
|
|
112
|
+
adData.ad.responsiveSearchAd = {
|
|
113
|
+
headlines: rsa.headlines.map(h => ({
|
|
114
|
+
text: h.text,
|
|
115
|
+
...(h.pinnedField ? { pinnedField: h.pinnedField } : {})
|
|
116
|
+
})),
|
|
117
|
+
descriptions: rsa.descriptions.map(d => ({
|
|
118
|
+
text: d.text,
|
|
119
|
+
...(d.pinnedField ? { pinnedField: d.pinnedField } : {})
|
|
120
|
+
}))
|
|
121
|
+
};
|
|
122
|
+
if (rsa.path1) adData.ad.responsiveSearchAd.path1 = rsa.path1;
|
|
123
|
+
if (rsa.path2) adData.ad.responsiveSearchAd.path2 = rsa.path2;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
let result = await client.mutateAdGroupAds(cid, [{ create: adData }]);
|
|
127
|
+
return {
|
|
128
|
+
output: {
|
|
129
|
+
adGroupAdResourceName: result.results?.[0]?.resourceName,
|
|
130
|
+
mutateResults: result
|
|
131
|
+
},
|
|
132
|
+
message: `Ad created in ad group **${ctx.input.adGroupId}**.`
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Update
|
|
137
|
+
if (!ctx.input.adGroupAdResourceName)
|
|
138
|
+
throw new Error('adGroupAdResourceName is required for update');
|
|
139
|
+
let updateData: Record<string, any> = { resourceName: ctx.input.adGroupAdResourceName };
|
|
140
|
+
let maskFields: string[] = [];
|
|
141
|
+
|
|
142
|
+
if (ctx.input.status !== undefined) {
|
|
143
|
+
updateData.status = ctx.input.status;
|
|
144
|
+
maskFields.push('status');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
let result = await client.mutateAdGroupAds(cid, [
|
|
148
|
+
{
|
|
149
|
+
update: updateData,
|
|
150
|
+
updateMask: maskFields.join(',')
|
|
151
|
+
}
|
|
152
|
+
]);
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
output: {
|
|
156
|
+
adGroupAdResourceName: ctx.input.adGroupAdResourceName,
|
|
157
|
+
mutateResults: result
|
|
158
|
+
},
|
|
159
|
+
message: `Ad updated (fields: ${maskFields.join(', ')}).`
|
|
160
|
+
};
|
|
161
|
+
})
|
|
162
|
+
.build();
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { SlateTool } from 'slates';
|
|
2
|
+
import { googleAdsActionScopes } from '../scopes';
|
|
3
|
+
import { spec } from '../spec';
|
|
4
|
+
import { createClient } from '../lib/helpers';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
|
|
7
|
+
export let manageAudienceLists = SlateTool.create(spec, {
|
|
8
|
+
name: 'Manage Audience Lists',
|
|
9
|
+
key: 'manage_audience_lists',
|
|
10
|
+
description: `Create, update, or remove user lists (audience segments) for targeting. Supports CRM-based customer lists, rule-based lists, and remarketing lists.
|
|
11
|
+
|
|
12
|
+
User lists can be applied to campaigns or ad groups for audience targeting, bid adjustments, or exclusions.`,
|
|
13
|
+
instructions: [
|
|
14
|
+
'For CRM-based lists, the membershipLifeSpan is in days (set to 10000 for no expiration).',
|
|
15
|
+
'Membership status can be OPEN (accepting new members) or CLOSED (not accepting).'
|
|
16
|
+
]
|
|
17
|
+
})
|
|
18
|
+
.scopes(googleAdsActionScopes.manageAudienceLists)
|
|
19
|
+
.input(
|
|
20
|
+
z.object({
|
|
21
|
+
customerId: z.string().describe('The Google Ads customer account ID'),
|
|
22
|
+
operation: z.enum(['create', 'update', 'remove']).describe('The operation to perform'),
|
|
23
|
+
userListId: z.string().optional().describe('User list ID (required for update/remove)'),
|
|
24
|
+
name: z.string().optional().describe('User list name'),
|
|
25
|
+
description: z.string().optional().describe('User list description'),
|
|
26
|
+
membershipLifeSpan: z
|
|
27
|
+
.number()
|
|
28
|
+
.optional()
|
|
29
|
+
.describe('Number of days a user stays in the list (set 10000 for unlimited)'),
|
|
30
|
+
membershipStatus: z
|
|
31
|
+
.enum(['OPEN', 'CLOSED'])
|
|
32
|
+
.optional()
|
|
33
|
+
.describe('Whether the list is open or closed for new members'),
|
|
34
|
+
listType: z
|
|
35
|
+
.enum(['CRM_BASED', 'RULE_BASED', 'LOGICAL'])
|
|
36
|
+
.optional()
|
|
37
|
+
.describe('Type of user list (required for create)'),
|
|
38
|
+
crmBasedUserList: z
|
|
39
|
+
.object({
|
|
40
|
+
uploadKeyType: z
|
|
41
|
+
.enum(['CONTACT_INFO', 'CRM_ID', 'MOBILE_ADVERTISING_ID'])
|
|
42
|
+
.optional()
|
|
43
|
+
.describe('Type of CRM data'),
|
|
44
|
+
dataSourceType: z
|
|
45
|
+
.enum(['FIRST_PARTY', 'THIRD_PARTY_CREDIT_BUREAU', 'THIRD_PARTY_VOTER_FILE'])
|
|
46
|
+
.optional()
|
|
47
|
+
.describe('Source of the CRM data')
|
|
48
|
+
})
|
|
49
|
+
.optional()
|
|
50
|
+
.describe('CRM-based user list configuration')
|
|
51
|
+
})
|
|
52
|
+
)
|
|
53
|
+
.output(
|
|
54
|
+
z.object({
|
|
55
|
+
userListResourceName: z.string().optional().describe('Resource name of the user list'),
|
|
56
|
+
mutateResults: z.any().optional().describe('Raw API response')
|
|
57
|
+
})
|
|
58
|
+
)
|
|
59
|
+
.handleInvocation(async ctx => {
|
|
60
|
+
let client = createClient(ctx.auth, ctx.config);
|
|
61
|
+
let { customerId, operation } = ctx.input;
|
|
62
|
+
let cid = customerId.replace(/-/g, '');
|
|
63
|
+
|
|
64
|
+
if (operation === 'remove') {
|
|
65
|
+
if (!ctx.input.userListId) throw new Error('userListId required');
|
|
66
|
+
let result = await client.mutateUserLists(cid, [
|
|
67
|
+
{
|
|
68
|
+
remove: `customers/${cid}/userLists/${ctx.input.userListId}`
|
|
69
|
+
}
|
|
70
|
+
]);
|
|
71
|
+
return {
|
|
72
|
+
output: { mutateResults: result },
|
|
73
|
+
message: `User list **${ctx.input.userListId}** removed.`
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (operation === 'create') {
|
|
78
|
+
let listData: Record<string, any> = {
|
|
79
|
+
name: ctx.input.name,
|
|
80
|
+
membershipStatus: ctx.input.membershipStatus || 'OPEN'
|
|
81
|
+
};
|
|
82
|
+
if (ctx.input.description) listData.description = ctx.input.description;
|
|
83
|
+
if (ctx.input.membershipLifeSpan)
|
|
84
|
+
listData.membershipLifeSpan = ctx.input.membershipLifeSpan;
|
|
85
|
+
if (ctx.input.listType === 'CRM_BASED' && ctx.input.crmBasedUserList) {
|
|
86
|
+
listData.crmBasedUserList = ctx.input.crmBasedUserList;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
let result = await client.mutateUserLists(cid, [{ create: listData }]);
|
|
90
|
+
return {
|
|
91
|
+
output: {
|
|
92
|
+
userListResourceName: result.results?.[0]?.resourceName,
|
|
93
|
+
mutateResults: result
|
|
94
|
+
},
|
|
95
|
+
message: `User list **${ctx.input.name}** created.`
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Update
|
|
100
|
+
if (!ctx.input.userListId) throw new Error('userListId required');
|
|
101
|
+
let resourceName = `customers/${cid}/userLists/${ctx.input.userListId}`;
|
|
102
|
+
let updateData: Record<string, any> = { resourceName };
|
|
103
|
+
let maskFields: string[] = [];
|
|
104
|
+
|
|
105
|
+
if (ctx.input.name !== undefined) {
|
|
106
|
+
updateData.name = ctx.input.name;
|
|
107
|
+
maskFields.push('name');
|
|
108
|
+
}
|
|
109
|
+
if (ctx.input.description !== undefined) {
|
|
110
|
+
updateData.description = ctx.input.description;
|
|
111
|
+
maskFields.push('description');
|
|
112
|
+
}
|
|
113
|
+
if (ctx.input.membershipLifeSpan !== undefined) {
|
|
114
|
+
updateData.membershipLifeSpan = ctx.input.membershipLifeSpan;
|
|
115
|
+
maskFields.push('membershipLifeSpan');
|
|
116
|
+
}
|
|
117
|
+
if (ctx.input.membershipStatus !== undefined) {
|
|
118
|
+
updateData.membershipStatus = ctx.input.membershipStatus;
|
|
119
|
+
maskFields.push('membershipStatus');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
let result = await client.mutateUserLists(cid, [
|
|
123
|
+
{
|
|
124
|
+
update: updateData,
|
|
125
|
+
updateMask: maskFields.join(',')
|
|
126
|
+
}
|
|
127
|
+
]);
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
output: { userListResourceName: resourceName, mutateResults: result },
|
|
131
|
+
message: `User list **${ctx.input.userListId}** updated.`
|
|
132
|
+
};
|
|
133
|
+
})
|
|
134
|
+
.build();
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { SlateTool } from 'slates';
|
|
2
|
+
import { googleAdsActionScopes } from '../scopes';
|
|
3
|
+
import { spec } from '../spec';
|
|
4
|
+
import { createClient } from '../lib/helpers';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
|
|
7
|
+
export let manageBiddingStrategies = SlateTool.create(spec, {
|
|
8
|
+
name: 'Manage Bidding Strategies',
|
|
9
|
+
key: 'manage_bidding_strategies',
|
|
10
|
+
description: `Create, update, or remove portfolio bidding strategies that can be shared across multiple campaigns. Portfolio strategies centralize bid management and enable cross-campaign optimization.
|
|
11
|
+
|
|
12
|
+
For campaign-level bidding, use the Manage Campaigns tool instead. This tool is specifically for shared/portfolio bidding strategies.`,
|
|
13
|
+
instructions: [
|
|
14
|
+
'Provide exactly one strategy configuration (targetCpa, targetRoas, maximizeConversions, maximizeConversionValue, or targetSpend).',
|
|
15
|
+
'Monetary values use micros (1 currency unit = 1,000,000 micros).'
|
|
16
|
+
]
|
|
17
|
+
})
|
|
18
|
+
.scopes(googleAdsActionScopes.manageBiddingStrategies)
|
|
19
|
+
.input(
|
|
20
|
+
z.object({
|
|
21
|
+
customerId: z.string().describe('The Google Ads customer account ID'),
|
|
22
|
+
operation: z.enum(['create', 'update', 'remove']).describe('The operation to perform'),
|
|
23
|
+
biddingStrategyId: z
|
|
24
|
+
.string()
|
|
25
|
+
.optional()
|
|
26
|
+
.describe('Bidding strategy ID (required for update/remove)'),
|
|
27
|
+
name: z.string().optional().describe('Name for the bidding strategy'),
|
|
28
|
+
targetCpa: z
|
|
29
|
+
.object({
|
|
30
|
+
targetCpaMicros: z.string().optional().describe('Target CPA in micros')
|
|
31
|
+
})
|
|
32
|
+
.optional()
|
|
33
|
+
.describe('Target CPA strategy configuration'),
|
|
34
|
+
targetRoas: z
|
|
35
|
+
.object({
|
|
36
|
+
targetRoas: z.number().optional().describe('Target ROAS value (e.g., 3.5 for 350%)')
|
|
37
|
+
})
|
|
38
|
+
.optional()
|
|
39
|
+
.describe('Target ROAS strategy configuration'),
|
|
40
|
+
maximizeConversions: z
|
|
41
|
+
.object({
|
|
42
|
+
targetCpaMicros: z.string().optional().describe('Optional target CPA limit')
|
|
43
|
+
})
|
|
44
|
+
.optional()
|
|
45
|
+
.describe('Maximize Conversions strategy configuration'),
|
|
46
|
+
maximizeConversionValue: z
|
|
47
|
+
.object({
|
|
48
|
+
targetRoas: z.number().optional().describe('Optional target ROAS limit')
|
|
49
|
+
})
|
|
50
|
+
.optional()
|
|
51
|
+
.describe('Maximize Conversion Value strategy configuration'),
|
|
52
|
+
targetSpend: z
|
|
53
|
+
.object({
|
|
54
|
+
cpcBidCeilingMicros: z
|
|
55
|
+
.string()
|
|
56
|
+
.optional()
|
|
57
|
+
.describe('Maximum CPC bid limit in micros')
|
|
58
|
+
})
|
|
59
|
+
.optional()
|
|
60
|
+
.describe('Target Spend strategy configuration')
|
|
61
|
+
})
|
|
62
|
+
)
|
|
63
|
+
.output(
|
|
64
|
+
z.object({
|
|
65
|
+
biddingStrategyResourceName: z
|
|
66
|
+
.string()
|
|
67
|
+
.optional()
|
|
68
|
+
.describe('Resource name of the bidding strategy'),
|
|
69
|
+
mutateResults: z.any().optional().describe('Raw API response')
|
|
70
|
+
})
|
|
71
|
+
)
|
|
72
|
+
.handleInvocation(async ctx => {
|
|
73
|
+
let client = createClient(ctx.auth, ctx.config);
|
|
74
|
+
let { customerId, operation } = ctx.input;
|
|
75
|
+
let cid = customerId.replace(/-/g, '');
|
|
76
|
+
|
|
77
|
+
if (operation === 'remove') {
|
|
78
|
+
if (!ctx.input.biddingStrategyId) throw new Error('biddingStrategyId required');
|
|
79
|
+
let result = await client.mutateBiddingStrategies(cid, [
|
|
80
|
+
{
|
|
81
|
+
remove: `customers/${cid}/biddingStrategies/${ctx.input.biddingStrategyId}`
|
|
82
|
+
}
|
|
83
|
+
]);
|
|
84
|
+
return {
|
|
85
|
+
output: { mutateResults: result },
|
|
86
|
+
message: `Bidding strategy **${ctx.input.biddingStrategyId}** removed.`
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (operation === 'create') {
|
|
91
|
+
let strategyData: Record<string, any> = { name: ctx.input.name };
|
|
92
|
+
if (ctx.input.targetCpa) strategyData.targetCpa = ctx.input.targetCpa;
|
|
93
|
+
if (ctx.input.targetRoas) strategyData.targetRoas = ctx.input.targetRoas;
|
|
94
|
+
if (ctx.input.maximizeConversions)
|
|
95
|
+
strategyData.maximizeConversions = ctx.input.maximizeConversions;
|
|
96
|
+
if (ctx.input.maximizeConversionValue)
|
|
97
|
+
strategyData.maximizeConversionValue = ctx.input.maximizeConversionValue;
|
|
98
|
+
if (ctx.input.targetSpend) strategyData.targetSpend = ctx.input.targetSpend;
|
|
99
|
+
|
|
100
|
+
let result = await client.mutateBiddingStrategies(cid, [{ create: strategyData }]);
|
|
101
|
+
return {
|
|
102
|
+
output: {
|
|
103
|
+
biddingStrategyResourceName: result.results?.[0]?.resourceName,
|
|
104
|
+
mutateResults: result
|
|
105
|
+
},
|
|
106
|
+
message: `Bidding strategy **${ctx.input.name}** created.`
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Update
|
|
111
|
+
if (!ctx.input.biddingStrategyId) throw new Error('biddingStrategyId required');
|
|
112
|
+
let resourceName = `customers/${cid}/biddingStrategies/${ctx.input.biddingStrategyId}`;
|
|
113
|
+
let updateData: Record<string, any> = { resourceName };
|
|
114
|
+
let maskFields: string[] = [];
|
|
115
|
+
|
|
116
|
+
if (ctx.input.name !== undefined) {
|
|
117
|
+
updateData.name = ctx.input.name;
|
|
118
|
+
maskFields.push('name');
|
|
119
|
+
}
|
|
120
|
+
if (ctx.input.targetCpa) {
|
|
121
|
+
updateData.targetCpa = ctx.input.targetCpa;
|
|
122
|
+
maskFields.push('targetCpa');
|
|
123
|
+
}
|
|
124
|
+
if (ctx.input.targetRoas) {
|
|
125
|
+
updateData.targetRoas = ctx.input.targetRoas;
|
|
126
|
+
maskFields.push('targetRoas');
|
|
127
|
+
}
|
|
128
|
+
if (ctx.input.maximizeConversions) {
|
|
129
|
+
updateData.maximizeConversions = ctx.input.maximizeConversions;
|
|
130
|
+
maskFields.push('maximizeConversions');
|
|
131
|
+
}
|
|
132
|
+
if (ctx.input.maximizeConversionValue) {
|
|
133
|
+
updateData.maximizeConversionValue = ctx.input.maximizeConversionValue;
|
|
134
|
+
maskFields.push('maximizeConversionValue');
|
|
135
|
+
}
|
|
136
|
+
if (ctx.input.targetSpend) {
|
|
137
|
+
updateData.targetSpend = ctx.input.targetSpend;
|
|
138
|
+
maskFields.push('targetSpend');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
let result = await client.mutateBiddingStrategies(cid, [
|
|
142
|
+
{
|
|
143
|
+
update: updateData,
|
|
144
|
+
updateMask: maskFields.join(',')
|
|
145
|
+
}
|
|
146
|
+
]);
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
output: { biddingStrategyResourceName: resourceName, mutateResults: result },
|
|
150
|
+
message: `Bidding strategy **${ctx.input.biddingStrategyId}** updated.`
|
|
151
|
+
};
|
|
152
|
+
})
|
|
153
|
+
.build();
|