@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.
@@ -0,0 +1,275 @@
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
+ let campaignStatusEnum = z.enum(['ENABLED', 'PAUSED', 'REMOVED']).describe('Campaign status');
8
+
9
+ let campaignTypeEnum = z
10
+ .enum([
11
+ 'SEARCH',
12
+ 'DISPLAY',
13
+ 'SHOPPING',
14
+ 'VIDEO',
15
+ 'PERFORMANCE_MAX',
16
+ 'MULTI_CHANNEL',
17
+ 'LOCAL',
18
+ 'SMART',
19
+ 'HOTEL',
20
+ 'LOCAL_SERVICES',
21
+ 'DISCOVERY',
22
+ 'TRAVEL',
23
+ 'DEMAND_GEN'
24
+ ])
25
+ .describe('Campaign advertising channel type');
26
+
27
+ export let manageCampaigns = SlateTool.create(spec, {
28
+ name: 'Manage Campaigns',
29
+ key: 'manage_campaigns',
30
+ description: `Create, update, or remove Google Ads campaigns. Supports setting campaign name, status, type, budget, start/end dates, bidding strategy, and network settings.
31
+
32
+ When creating a campaign, a campaign budget is automatically created if \`dailyBudgetMicros\` is provided. For updating, only the specified fields are modified.`,
33
+ instructions: [
34
+ 'Monetary values use micros (1 currency unit = 1,000,000 micros). For example, $5.00 = 5000000 micros.',
35
+ 'When removing a campaign, only the campaignId is required.',
36
+ 'Campaign resource names follow the format: customers/{customerId}/campaigns/{campaignId}'
37
+ ],
38
+ constraints: ['Campaign type cannot be changed after creation.']
39
+ })
40
+ .scopes(googleAdsActionScopes.manageCampaigns)
41
+ .input(
42
+ z.object({
43
+ customerId: z.string().describe('The Google Ads customer account ID (without hyphens)'),
44
+ operation: z.enum(['create', 'update', 'remove']).describe('The operation to perform'),
45
+ campaignId: z.string().optional().describe('Campaign ID (required for update/remove)'),
46
+ name: z.string().optional().describe('Campaign name (required for create)'),
47
+ status: campaignStatusEnum.optional(),
48
+ advertisingChannelType: campaignTypeEnum
49
+ .optional()
50
+ .describe('Campaign type (required for create)'),
51
+ dailyBudgetMicros: z
52
+ .string()
53
+ .optional()
54
+ .describe(
55
+ 'Daily budget in micros (e.g., "5000000" for $5.00). A budget resource is created automatically for new campaigns.'
56
+ ),
57
+ existingBudgetResourceName: z
58
+ .string()
59
+ .optional()
60
+ .describe(
61
+ 'Resource name of an existing campaign budget to use instead of creating one'
62
+ ),
63
+ startDate: z.string().optional().describe('Campaign start date in YYYY-MM-DD format'),
64
+ endDate: z.string().optional().describe('Campaign end date in YYYY-MM-DD format'),
65
+ biddingStrategyType: z
66
+ .string()
67
+ .optional()
68
+ .describe(
69
+ 'Bidding strategy type, e.g., MANUAL_CPC, MAXIMIZE_CONVERSIONS, TARGET_CPA, TARGET_ROAS'
70
+ ),
71
+ targetCpaMicros: z
72
+ .string()
73
+ .optional()
74
+ .describe('Target CPA in micros (for TARGET_CPA bidding)'),
75
+ targetRoas: z
76
+ .number()
77
+ .optional()
78
+ .describe('Target ROAS value (for TARGET_ROAS bidding, e.g., 3.5 for 350% ROAS)'),
79
+ networkSettings: z
80
+ .object({
81
+ targetGoogleSearch: z.boolean().optional(),
82
+ targetSearchNetwork: z.boolean().optional(),
83
+ targetContentNetwork: z.boolean().optional(),
84
+ targetPartnerSearchNetwork: z.boolean().optional()
85
+ })
86
+ .optional()
87
+ .describe('Network targeting settings')
88
+ })
89
+ )
90
+ .output(
91
+ z.object({
92
+ campaignResourceName: z
93
+ .string()
94
+ .optional()
95
+ .describe('Resource name of the created/updated campaign'),
96
+ budgetResourceName: z
97
+ .string()
98
+ .optional()
99
+ .describe('Resource name of the created budget (if applicable)'),
100
+ mutateResults: z.any().optional().describe('Raw API mutate response')
101
+ })
102
+ )
103
+ .handleInvocation(async ctx => {
104
+ let client = createClient(ctx.auth, ctx.config);
105
+ let { customerId, operation } = ctx.input;
106
+ let cid = customerId.replace(/-/g, '');
107
+
108
+ if (operation === 'remove') {
109
+ if (!ctx.input.campaignId)
110
+ throw new Error('campaignId is required for remove operation');
111
+ let result = await client.mutateCampaigns(cid, [
112
+ {
113
+ remove: `customers/${cid}/campaigns/${ctx.input.campaignId}`
114
+ }
115
+ ]);
116
+ return {
117
+ output: {
118
+ campaignResourceName: `customers/${cid}/campaigns/${ctx.input.campaignId}`,
119
+ mutateResults: result
120
+ },
121
+ message: `Campaign **${ctx.input.campaignId}** has been removed.`
122
+ };
123
+ }
124
+
125
+ if (operation === 'create') {
126
+ let budgetResourceName = ctx.input.existingBudgetResourceName;
127
+
128
+ if (!budgetResourceName && ctx.input.dailyBudgetMicros) {
129
+ let budgetResult = await client.mutateCampaignBudgets(cid, [
130
+ {
131
+ create: {
132
+ amountMicros: ctx.input.dailyBudgetMicros,
133
+ deliveryMethod: 'STANDARD',
134
+ explicitlyShared: false
135
+ }
136
+ }
137
+ ]);
138
+ budgetResourceName = budgetResult.results?.[0]?.resourceName;
139
+ }
140
+
141
+ let campaignData: Record<string, any> = {
142
+ name: ctx.input.name,
143
+ advertisingChannelType: ctx.input.advertisingChannelType,
144
+ status: ctx.input.status || 'PAUSED'
145
+ };
146
+
147
+ if (budgetResourceName) campaignData.campaignBudget = budgetResourceName;
148
+ if (ctx.input.startDate) campaignData.startDate = ctx.input.startDate;
149
+ if (ctx.input.endDate) campaignData.endDate = ctx.input.endDate;
150
+ if (ctx.input.networkSettings) campaignData.networkSettings = ctx.input.networkSettings;
151
+
152
+ if (ctx.input.biddingStrategyType) {
153
+ switch (ctx.input.biddingStrategyType) {
154
+ case 'MANUAL_CPC':
155
+ campaignData.manualCpc = {};
156
+ break;
157
+ case 'MANUAL_CPM':
158
+ campaignData.manualCpm = {};
159
+ break;
160
+ case 'MAXIMIZE_CONVERSIONS':
161
+ campaignData.maximizeConversions = ctx.input.targetCpaMicros
162
+ ? { targetCpaMicros: ctx.input.targetCpaMicros }
163
+ : {};
164
+ break;
165
+ case 'MAXIMIZE_CONVERSION_VALUE':
166
+ campaignData.maximizeConversionValue = ctx.input.targetRoas
167
+ ? { targetRoas: ctx.input.targetRoas }
168
+ : {};
169
+ break;
170
+ case 'TARGET_CPA':
171
+ campaignData.targetCpa = { targetCpaMicros: ctx.input.targetCpaMicros };
172
+ break;
173
+ case 'TARGET_ROAS':
174
+ campaignData.targetRoas = { targetRoas: ctx.input.targetRoas };
175
+ break;
176
+ case 'TARGET_SPEND':
177
+ campaignData.targetSpend = {};
178
+ break;
179
+ }
180
+ }
181
+
182
+ let result = await client.mutateCampaigns(cid, [{ create: campaignData }]);
183
+ let campaignResourceName = result.results?.[0]?.resourceName;
184
+
185
+ return {
186
+ output: {
187
+ campaignResourceName,
188
+ budgetResourceName,
189
+ mutateResults: result
190
+ },
191
+ message: `Campaign **${ctx.input.name}** created successfully.`
192
+ };
193
+ }
194
+
195
+ // Update
196
+ if (!ctx.input.campaignId) throw new Error('campaignId is required for update operation');
197
+
198
+ let resourceName = `customers/${cid}/campaigns/${ctx.input.campaignId}`;
199
+ let updateData: Record<string, any> = { resourceName };
200
+ let updateMaskFields: string[] = [];
201
+
202
+ if (ctx.input.name !== undefined) {
203
+ updateData.name = ctx.input.name;
204
+ updateMaskFields.push('name');
205
+ }
206
+ if (ctx.input.status !== undefined) {
207
+ updateData.status = ctx.input.status;
208
+ updateMaskFields.push('status');
209
+ }
210
+ if (ctx.input.startDate !== undefined) {
211
+ updateData.startDate = ctx.input.startDate;
212
+ updateMaskFields.push('startDate');
213
+ }
214
+ if (ctx.input.endDate !== undefined) {
215
+ updateData.endDate = ctx.input.endDate;
216
+ updateMaskFields.push('endDate');
217
+ }
218
+ if (ctx.input.networkSettings !== undefined) {
219
+ updateData.networkSettings = ctx.input.networkSettings;
220
+ updateMaskFields.push('networkSettings');
221
+ }
222
+ if (ctx.input.existingBudgetResourceName !== undefined) {
223
+ updateData.campaignBudget = ctx.input.existingBudgetResourceName;
224
+ updateMaskFields.push('campaignBudget');
225
+ }
226
+
227
+ if (ctx.input.biddingStrategyType) {
228
+ switch (ctx.input.biddingStrategyType) {
229
+ case 'MANUAL_CPC':
230
+ updateData.manualCpc = {};
231
+ updateMaskFields.push('manualCpc');
232
+ break;
233
+ case 'MAXIMIZE_CONVERSIONS':
234
+ updateData.maximizeConversions = ctx.input.targetCpaMicros
235
+ ? { targetCpaMicros: ctx.input.targetCpaMicros }
236
+ : {};
237
+ updateMaskFields.push('maximizeConversions');
238
+ break;
239
+ case 'MAXIMIZE_CONVERSION_VALUE':
240
+ updateData.maximizeConversionValue = ctx.input.targetRoas
241
+ ? { targetRoas: ctx.input.targetRoas }
242
+ : {};
243
+ updateMaskFields.push('maximizeConversionValue');
244
+ break;
245
+ case 'TARGET_CPA':
246
+ updateData.targetCpa = { targetCpaMicros: ctx.input.targetCpaMicros };
247
+ updateMaskFields.push('targetCpa');
248
+ break;
249
+ case 'TARGET_ROAS':
250
+ updateData.targetRoas = { targetRoas: ctx.input.targetRoas };
251
+ updateMaskFields.push('targetRoas');
252
+ break;
253
+ case 'TARGET_SPEND':
254
+ updateData.targetSpend = {};
255
+ updateMaskFields.push('targetSpend');
256
+ break;
257
+ }
258
+ }
259
+
260
+ let result = await client.mutateCampaigns(cid, [
261
+ {
262
+ update: updateData,
263
+ updateMask: updateMaskFields.join(',')
264
+ }
265
+ ]);
266
+
267
+ return {
268
+ output: {
269
+ campaignResourceName: resourceName,
270
+ mutateResults: result
271
+ },
272
+ message: `Campaign **${ctx.input.campaignId}** updated (fields: ${updateMaskFields.join(', ')}).`
273
+ };
274
+ })
275
+ .build();
@@ -0,0 +1,224 @@
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 manageConversionActions = SlateTool.create(spec, {
8
+ name: 'Manage Conversion Actions',
9
+ key: 'manage_conversion_actions',
10
+ description: `Create, update, or remove conversion actions for tracking valuable customer actions. Conversion actions track events like purchases, sign-ups, phone calls, or app installs.
11
+
12
+ Supports configuring conversion counting, attribution models, value settings, and conversion windows.`,
13
+ instructions: [
14
+ 'Common conversion types: PURCHASE, SIGNUP, LEAD, PAGE_VIEW, DOWNLOAD, OTHER',
15
+ 'Categories: DEFAULT, PAGE_VIEW, PURCHASE, SIGNUP, LEAD, DOWNLOAD, ADD_TO_CART, BEGIN_CHECKOUT, SUBSCRIBE_PAID, PHONE_CALL_LEAD, IMPORTED_LEAD, SUBMIT_LEAD_FORM, BOOK_APPOINTMENT, REQUEST_QUOTE, GET_DIRECTIONS, OUTBOUND_CLICK, CONTACT, ENGAGEMENT, STORE_VISIT, STORE_SALE'
16
+ ]
17
+ })
18
+ .scopes(googleAdsActionScopes.manageConversionActions)
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
+ conversionActionId: z
24
+ .string()
25
+ .optional()
26
+ .describe('Conversion action ID (required for update/remove)'),
27
+ name: z.string().optional().describe('Name of the conversion action'),
28
+ type: z
29
+ .enum([
30
+ 'AD_CALL',
31
+ 'CLICK_TO_CALL',
32
+ 'GOOGLE_PLAY_DOWNLOAD',
33
+ 'GOOGLE_PLAY_IN_APP_PURCHASE',
34
+ 'UPLOAD',
35
+ 'UPLOAD_CALLS',
36
+ 'WEBPAGE',
37
+ 'WEBSITE_CALL',
38
+ 'STORE_SALES_DIRECT_UPLOAD',
39
+ 'STORE_SALES',
40
+ 'FIREBASE_ANDROID_FIRST_OPEN',
41
+ 'FIREBASE_ANDROID_IN_APP_PURCHASE',
42
+ 'FIREBASE_IOS_FIRST_OPEN',
43
+ 'FIREBASE_IOS_IN_APP_PURCHASE',
44
+ 'GOOGLE_HOSTED',
45
+ 'LEAD_FORM_SUBMIT',
46
+ 'SALESFORCE',
47
+ 'SEARCH_ADS_360',
48
+ 'SMART_CAMPAIGN_AD_CLICKS_TO_CALL',
49
+ 'SMART_CAMPAIGN_MAP_CLICKS_TO_CALL',
50
+ 'SMART_CAMPAIGN_MAP_DIRECTIONS',
51
+ 'SMART_CAMPAIGN_TRACKED_CALLS',
52
+ 'STORE_VISITS',
53
+ 'WEBPAGE_CODELESS'
54
+ ])
55
+ .optional()
56
+ .describe('Conversion action type (required for create)'),
57
+ category: z.string().optional().describe('Conversion category'),
58
+ status: z
59
+ .enum(['ENABLED', 'REMOVED', 'HIDDEN'])
60
+ .optional()
61
+ .describe('Conversion action status'),
62
+ countingType: z
63
+ .enum(['ONE_PER_CLICK', 'MANY_PER_CLICK'])
64
+ .optional()
65
+ .describe('How conversions are counted'),
66
+ defaultValue: z.number().optional().describe('Default conversion value'),
67
+ alwaysUseDefaultValue: z
68
+ .boolean()
69
+ .optional()
70
+ .describe('Whether to always use the default value'),
71
+ clickThroughLookbackWindowDays: z
72
+ .number()
73
+ .optional()
74
+ .describe('Click-through conversion window in days (1-90)'),
75
+ viewThroughLookbackWindowDays: z
76
+ .number()
77
+ .optional()
78
+ .describe('View-through conversion window in days (1-90)'),
79
+ attributionModel: z
80
+ .enum([
81
+ 'EXTERNAL',
82
+ 'GOOGLE_ADS_LAST_CLICK',
83
+ 'GOOGLE_SEARCH_ATTRIBUTION_FIRST_CLICK',
84
+ 'GOOGLE_SEARCH_ATTRIBUTION_LINEAR',
85
+ 'GOOGLE_SEARCH_ATTRIBUTION_TIME_DECAY',
86
+ 'GOOGLE_SEARCH_ATTRIBUTION_POSITION_BASED',
87
+ 'GOOGLE_SEARCH_ATTRIBUTION_DATA_DRIVEN'
88
+ ])
89
+ .optional()
90
+ .describe('Attribution model'),
91
+ includeInConversionsMetric: z
92
+ .boolean()
93
+ .optional()
94
+ .describe('Whether to include in the "Conversions" column')
95
+ })
96
+ )
97
+ .output(
98
+ z.object({
99
+ conversionActionResourceName: z
100
+ .string()
101
+ .optional()
102
+ .describe('Resource name of the conversion action'),
103
+ mutateResults: z.any().optional().describe('Raw API response')
104
+ })
105
+ )
106
+ .handleInvocation(async ctx => {
107
+ let client = createClient(ctx.auth, ctx.config);
108
+ let { customerId, operation } = ctx.input;
109
+ let cid = customerId.replace(/-/g, '');
110
+
111
+ if (operation === 'remove') {
112
+ if (!ctx.input.conversionActionId) throw new Error('conversionActionId required');
113
+ let result = await client.mutateConversionActions(cid, [
114
+ {
115
+ remove: `customers/${cid}/conversionActions/${ctx.input.conversionActionId}`
116
+ }
117
+ ]);
118
+ return {
119
+ output: { mutateResults: result },
120
+ message: `Conversion action **${ctx.input.conversionActionId}** removed.`
121
+ };
122
+ }
123
+
124
+ if (operation === 'create') {
125
+ let actionData: Record<string, any> = {
126
+ name: ctx.input.name,
127
+ type: ctx.input.type,
128
+ status: ctx.input.status || 'ENABLED'
129
+ };
130
+ if (ctx.input.category) actionData.category = ctx.input.category;
131
+ if (ctx.input.countingType) actionData.countingType = ctx.input.countingType;
132
+ if (
133
+ ctx.input.defaultValue !== undefined ||
134
+ ctx.input.alwaysUseDefaultValue !== undefined
135
+ ) {
136
+ actionData.valueSettings = {};
137
+ if (ctx.input.defaultValue !== undefined)
138
+ actionData.valueSettings.defaultValue = ctx.input.defaultValue;
139
+ if (ctx.input.alwaysUseDefaultValue !== undefined)
140
+ actionData.valueSettings.alwaysUseDefaultValue = ctx.input.alwaysUseDefaultValue;
141
+ }
142
+ if (ctx.input.clickThroughLookbackWindowDays)
143
+ actionData.clickThroughLookbackWindowDays = ctx.input.clickThroughLookbackWindowDays;
144
+ if (ctx.input.viewThroughLookbackWindowDays)
145
+ actionData.viewThroughLookbackWindowDays = ctx.input.viewThroughLookbackWindowDays;
146
+ if (ctx.input.attributionModel) {
147
+ actionData.attributionModelSettings = { attributionModel: ctx.input.attributionModel };
148
+ }
149
+ if (ctx.input.includeInConversionsMetric !== undefined)
150
+ actionData.includeInConversionsMetric = ctx.input.includeInConversionsMetric;
151
+
152
+ let result = await client.mutateConversionActions(cid, [{ create: actionData }]);
153
+ return {
154
+ output: {
155
+ conversionActionResourceName: result.results?.[0]?.resourceName,
156
+ mutateResults: result
157
+ },
158
+ message: `Conversion action **${ctx.input.name}** created.`
159
+ };
160
+ }
161
+
162
+ // Update
163
+ if (!ctx.input.conversionActionId) throw new Error('conversionActionId required');
164
+ let resourceName = `customers/${cid}/conversionActions/${ctx.input.conversionActionId}`;
165
+ let updateData: Record<string, any> = { resourceName };
166
+ let maskFields: string[] = [];
167
+
168
+ if (ctx.input.name !== undefined) {
169
+ updateData.name = ctx.input.name;
170
+ maskFields.push('name');
171
+ }
172
+ if (ctx.input.status !== undefined) {
173
+ updateData.status = ctx.input.status;
174
+ maskFields.push('status');
175
+ }
176
+ if (ctx.input.category !== undefined) {
177
+ updateData.category = ctx.input.category;
178
+ maskFields.push('category');
179
+ }
180
+ if (ctx.input.countingType !== undefined) {
181
+ updateData.countingType = ctx.input.countingType;
182
+ maskFields.push('countingType');
183
+ }
184
+ if (
185
+ ctx.input.defaultValue !== undefined ||
186
+ ctx.input.alwaysUseDefaultValue !== undefined
187
+ ) {
188
+ updateData.valueSettings = {};
189
+ if (ctx.input.defaultValue !== undefined)
190
+ updateData.valueSettings.defaultValue = ctx.input.defaultValue;
191
+ if (ctx.input.alwaysUseDefaultValue !== undefined)
192
+ updateData.valueSettings.alwaysUseDefaultValue = ctx.input.alwaysUseDefaultValue;
193
+ maskFields.push('valueSettings');
194
+ }
195
+ if (ctx.input.clickThroughLookbackWindowDays !== undefined) {
196
+ updateData.clickThroughLookbackWindowDays = ctx.input.clickThroughLookbackWindowDays;
197
+ maskFields.push('clickThroughLookbackWindowDays');
198
+ }
199
+ if (ctx.input.viewThroughLookbackWindowDays !== undefined) {
200
+ updateData.viewThroughLookbackWindowDays = ctx.input.viewThroughLookbackWindowDays;
201
+ maskFields.push('viewThroughLookbackWindowDays');
202
+ }
203
+ if (ctx.input.attributionModel !== undefined) {
204
+ updateData.attributionModelSettings = { attributionModel: ctx.input.attributionModel };
205
+ maskFields.push('attributionModelSettings');
206
+ }
207
+ if (ctx.input.includeInConversionsMetric !== undefined) {
208
+ updateData.includeInConversionsMetric = ctx.input.includeInConversionsMetric;
209
+ maskFields.push('includeInConversionsMetric');
210
+ }
211
+
212
+ let result = await client.mutateConversionActions(cid, [
213
+ {
214
+ update: updateData,
215
+ updateMask: maskFields.join(',')
216
+ }
217
+ ]);
218
+
219
+ return {
220
+ output: { conversionActionResourceName: resourceName, mutateResults: result },
221
+ message: `Conversion action **${ctx.input.conversionActionId}** updated (fields: ${maskFields.join(', ')}).`
222
+ };
223
+ })
224
+ .build();
@@ -0,0 +1,167 @@
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 manageKeywords = SlateTool.create(spec, {
8
+ name: 'Manage Keywords',
9
+ key: 'manage_keywords',
10
+ description: `Add, update, or remove keywords in an ad group. Also supports managing negative keywords at both the ad group and campaign levels.
11
+
12
+ Keywords determine when ads are shown based on user search queries. Each keyword has a match type controlling how broadly it matches search terms.`,
13
+ instructions: [
14
+ 'Match types: EXACT (most restrictive), PHRASE (moderate), BROAD (widest reach).',
15
+ 'Use negative keywords to prevent ads from showing for irrelevant searches.',
16
+ 'Set isNegative to true and provide campaignId (for campaign-level) or adGroupId (for ad group-level) negative keywords.'
17
+ ]
18
+ })
19
+ .scopes(googleAdsActionScopes.manageKeywords)
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
25
+ .string()
26
+ .optional()
27
+ .describe('Ad group ID (required for ad group keywords)'),
28
+ campaignId: z
29
+ .string()
30
+ .optional()
31
+ .describe('Campaign ID (required for campaign-level negative keywords)'),
32
+ criterionId: z.string().optional().describe('Criterion ID (required for update/remove)'),
33
+ keyword: z.string().optional().describe('The keyword text'),
34
+ matchType: z
35
+ .enum(['EXACT', 'PHRASE', 'BROAD'])
36
+ .optional()
37
+ .describe('Keyword match type'),
38
+ isNegative: z.boolean().optional().describe('If true, creates a negative keyword'),
39
+ cpcBidMicros: z.string().optional().describe('Keyword-level CPC bid in micros'),
40
+ status: z.enum(['ENABLED', 'PAUSED', 'REMOVED']).optional().describe('Keyword status'),
41
+ finalUrls: z.array(z.string()).optional().describe('Final URLs for the keyword')
42
+ })
43
+ )
44
+ .output(
45
+ z.object({
46
+ criterionResourceName: z
47
+ .string()
48
+ .optional()
49
+ .describe('Resource name of the keyword criterion'),
50
+ mutateResults: z.any().optional().describe('Raw API response')
51
+ })
52
+ )
53
+ .handleInvocation(async ctx => {
54
+ let client = createClient(ctx.auth, ctx.config);
55
+ let { customerId, operation } = ctx.input;
56
+ let cid = customerId.replace(/-/g, '');
57
+
58
+ // Campaign-level negative keywords
59
+ if (ctx.input.isNegative && ctx.input.campaignId) {
60
+ if (operation === 'remove') {
61
+ if (!ctx.input.criterionId) throw new Error('criterionId required for remove');
62
+ let result = await client.mutateCampaignCriteria(cid, [
63
+ {
64
+ remove: `customers/${cid}/campaignCriteria/${ctx.input.campaignId}~${ctx.input.criterionId}`
65
+ }
66
+ ]);
67
+ return {
68
+ output: { mutateResults: result },
69
+ message: `Campaign negative keyword removed.`
70
+ };
71
+ }
72
+
73
+ if (operation === 'create') {
74
+ let criterionData: Record<string, any> = {
75
+ campaign: `customers/${cid}/campaigns/${ctx.input.campaignId}`,
76
+ negative: true,
77
+ keyword: {
78
+ text: ctx.input.keyword,
79
+ matchType: ctx.input.matchType || 'BROAD'
80
+ }
81
+ };
82
+ let result = await client.mutateCampaignCriteria(cid, [{ create: criterionData }]);
83
+ return {
84
+ output: {
85
+ criterionResourceName: result.results?.[0]?.resourceName,
86
+ mutateResults: result
87
+ },
88
+ message: `Campaign-level negative keyword **"${ctx.input.keyword}"** added.`
89
+ };
90
+ }
91
+ }
92
+
93
+ // Ad group keywords
94
+ if (operation === 'remove') {
95
+ if (!ctx.input.adGroupId || !ctx.input.criterionId)
96
+ throw new Error('adGroupId and criterionId required for remove');
97
+ let result = await client.mutateAdGroupCriteria(cid, [
98
+ {
99
+ remove: `customers/${cid}/adGroupCriteria/${ctx.input.adGroupId}~${ctx.input.criterionId}`
100
+ }
101
+ ]);
102
+ return {
103
+ output: {
104
+ criterionResourceName: `customers/${cid}/adGroupCriteria/${ctx.input.adGroupId}~${ctx.input.criterionId}`,
105
+ mutateResults: result
106
+ },
107
+ message: `Keyword criterion removed.`
108
+ };
109
+ }
110
+
111
+ if (operation === 'create') {
112
+ if (!ctx.input.adGroupId) throw new Error('adGroupId is required for create');
113
+ let criterionData: Record<string, any> = {
114
+ adGroup: `customers/${cid}/adGroups/${ctx.input.adGroupId}`,
115
+ status: ctx.input.status || 'ENABLED',
116
+ keyword: {
117
+ text: ctx.input.keyword,
118
+ matchType: ctx.input.matchType || 'BROAD'
119
+ }
120
+ };
121
+ if (ctx.input.isNegative) criterionData.negative = true;
122
+ if (ctx.input.cpcBidMicros) criterionData.cpcBidMicros = ctx.input.cpcBidMicros;
123
+ if (ctx.input.finalUrls) criterionData.finalUrls = ctx.input.finalUrls;
124
+
125
+ let result = await client.mutateAdGroupCriteria(cid, [{ create: criterionData }]);
126
+ return {
127
+ output: {
128
+ criterionResourceName: result.results?.[0]?.resourceName,
129
+ mutateResults: result
130
+ },
131
+ message: `Keyword **"${ctx.input.keyword}"** (${ctx.input.matchType || 'BROAD'}) added to ad group.`
132
+ };
133
+ }
134
+
135
+ // Update
136
+ if (!ctx.input.adGroupId || !ctx.input.criterionId)
137
+ throw new Error('adGroupId and criterionId required for update');
138
+ let resourceName = `customers/${cid}/adGroupCriteria/${ctx.input.adGroupId}~${ctx.input.criterionId}`;
139
+ let updateData: Record<string, any> = { resourceName };
140
+ let maskFields: string[] = [];
141
+
142
+ if (ctx.input.status !== undefined) {
143
+ updateData.status = ctx.input.status;
144
+ maskFields.push('status');
145
+ }
146
+ if (ctx.input.cpcBidMicros !== undefined) {
147
+ updateData.cpcBidMicros = ctx.input.cpcBidMicros;
148
+ maskFields.push('cpcBidMicros');
149
+ }
150
+ if (ctx.input.finalUrls !== undefined) {
151
+ updateData.finalUrls = ctx.input.finalUrls;
152
+ maskFields.push('finalUrls');
153
+ }
154
+
155
+ let result = await client.mutateAdGroupCriteria(cid, [
156
+ {
157
+ update: updateData,
158
+ updateMask: maskFields.join(',')
159
+ }
160
+ ]);
161
+
162
+ return {
163
+ output: { criterionResourceName: resourceName, mutateResults: result },
164
+ message: `Keyword criterion updated (fields: ${maskFields.join(', ')}).`
165
+ };
166
+ })
167
+ .build();