@feastalytics/cli 0.1.10 → 0.1.11

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/dist/cli.js CHANGED
@@ -10522,6 +10522,119 @@ var CLI_MANIFEST = {
10522
10522
  "resetProgress"
10523
10523
  ],
10524
10524
  "additionalProperties": false
10525
+ },
10526
+ {
10527
+ "type": "object",
10528
+ "properties": {
10529
+ "type": {
10530
+ "type": "string",
10531
+ "const": "setCustomProperty"
10532
+ },
10533
+ "setCustomProperty": {
10534
+ "type": "object",
10535
+ "properties": {
10536
+ "propertyId": {
10537
+ "type": "string"
10538
+ },
10539
+ "operation": {
10540
+ "anyOf": [
10541
+ {
10542
+ "type": "object",
10543
+ "properties": {
10544
+ "type": {
10545
+ "type": "string",
10546
+ "const": "set"
10547
+ },
10548
+ "set": {
10549
+ "type": "object",
10550
+ "properties": {
10551
+ "booleanValue": {
10552
+ "type": [
10553
+ "boolean",
10554
+ "null"
10555
+ ]
10556
+ },
10557
+ "isDateValue": {
10558
+ "anyOf": [
10559
+ {
10560
+ "type": "string",
10561
+ "format": "date-time"
10562
+ },
10563
+ {
10564
+ "type": "null"
10565
+ }
10566
+ ]
10567
+ },
10568
+ "localDayOfYear": {
10569
+ "type": [
10570
+ "string",
10571
+ "null"
10572
+ ]
10573
+ },
10574
+ "numberValue": {
10575
+ "type": [
10576
+ "number",
10577
+ "null"
10578
+ ]
10579
+ },
10580
+ "stringValue": {
10581
+ "type": [
10582
+ "string",
10583
+ "null"
10584
+ ]
10585
+ }
10586
+ },
10587
+ "additionalProperties": false
10588
+ }
10589
+ },
10590
+ "required": [
10591
+ "type",
10592
+ "set"
10593
+ ],
10594
+ "additionalProperties": false
10595
+ },
10596
+ {
10597
+ "type": "object",
10598
+ "properties": {
10599
+ "type": {
10600
+ "type": "string",
10601
+ "const": "increment"
10602
+ },
10603
+ "increment": {
10604
+ "type": "object",
10605
+ "properties": {
10606
+ "amount": {
10607
+ "type": "number"
10608
+ }
10609
+ },
10610
+ "required": [
10611
+ "amount"
10612
+ ],
10613
+ "additionalProperties": false,
10614
+ "description": "Number properties only. Added to the current value, or to 0."
10615
+ }
10616
+ },
10617
+ "required": [
10618
+ "type",
10619
+ "increment"
10620
+ ],
10621
+ "additionalProperties": false
10622
+ }
10623
+ ]
10624
+ }
10625
+ },
10626
+ "required": [
10627
+ "propertyId",
10628
+ "operation"
10629
+ ],
10630
+ "additionalProperties": false
10631
+ }
10632
+ },
10633
+ "required": [
10634
+ "type",
10635
+ "setCustomProperty"
10636
+ ],
10637
+ "additionalProperties": false
10525
10638
  }
10526
10639
  ]
10527
10640
  }
@@ -12301,6 +12414,119 @@ var CLI_MANIFEST = {
12301
12414
  "resetProgress"
12302
12415
  ],
12303
12416
  "additionalProperties": false
12417
+ },
12418
+ {
12419
+ "type": "object",
12420
+ "properties": {
12421
+ "type": {
12422
+ "type": "string",
12423
+ "const": "setCustomProperty"
12424
+ },
12425
+ "setCustomProperty": {
12426
+ "type": "object",
12427
+ "properties": {
12428
+ "propertyId": {
12429
+ "type": "string"
12430
+ },
12431
+ "operation": {
12432
+ "anyOf": [
12433
+ {
12434
+ "type": "object",
12435
+ "properties": {
12436
+ "type": {
12437
+ "type": "string",
12438
+ "const": "set"
12439
+ },
12440
+ "set": {
12441
+ "type": "object",
12442
+ "properties": {
12443
+ "booleanValue": {
12444
+ "type": [
12445
+ "boolean",
12446
+ "null"
12447
+ ]
12448
+ },
12449
+ "isDateValue": {
12450
+ "anyOf": [
12451
+ {
12452
+ "type": "string",
12453
+ "format": "date-time"
12454
+ },
12455
+ {
12456
+ "type": "null"
12457
+ }
12458
+ ]
12459
+ },
12460
+ "localDayOfYear": {
12461
+ "type": [
12462
+ "string",
12463
+ "null"
12464
+ ]
12465
+ },
12466
+ "numberValue": {
12467
+ "type": [
12468
+ "number",
12469
+ "null"
12470
+ ]
12471
+ },
12472
+ "stringValue": {
12473
+ "type": [
12474
+ "string",
12475
+ "null"
12476
+ ]
12477
+ }
12478
+ },
12479
+ "additionalProperties": false
12480
+ }
12481
+ },
12482
+ "required": [
12483
+ "type",
12484
+ "set"
12485
+ ],
12486
+ "additionalProperties": false
12487
+ },
12488
+ {
12489
+ "type": "object",
12490
+ "properties": {
12491
+ "type": {
12492
+ "type": "string",
12493
+ "const": "increment"
12494
+ },
12495
+ "increment": {
12496
+ "type": "object",
12497
+ "properties": {
12498
+ "amount": {
12499
+ "type": "number"
12500
+ }
12501
+ },
12502
+ "required": [
12503
+ "amount"
12504
+ ],
12505
+ "additionalProperties": false,
12506
+ "description": "Number properties only. Added to the current value, or to 0."
12507
+ }
12508
+ },
12509
+ "required": [
12510
+ "type",
12511
+ "increment"
12512
+ ],
12513
+ "additionalProperties": false
12514
+ }
12515
+ ]
12516
+ }
12517
+ },
12518
+ "required": [
12519
+ "propertyId",
12520
+ "operation"
12521
+ ],
12522
+ "additionalProperties": false
12523
+ }
12524
+ },
12525
+ "required": [
12526
+ "type",
12527
+ "setCustomProperty"
12528
+ ],
12529
+ "additionalProperties": false
12304
12530
  }
12305
12531
  ]
12306
12532
  }
@@ -14341,6 +14567,32 @@ var CLI_MANIFEST = {
14341
14567
  "$schema": "http://json-schema.org/draft-07/schema#"
14342
14568
  }
14343
14569
  },
14570
+ {
14571
+ "id": "listIgMedia",
14572
+ "domain": "ads",
14573
+ "description": "List recent posts from the Instagram business account linked to a Facebook Page, for use as igMedia creative references in planAds templates. Returns the instagramUserId to put on the igMedia creative ref plus up to 50 recent posts with id, caption, thumbnail, mediaType, permalink and timestamp. Takes a pageId from ads_get_user_pages; a Page with no linked Instagram business account returns instagramAccount null.",
14574
+ "type": "query",
14575
+ "path": [
14576
+ "api",
14577
+ "ads",
14578
+ "facebook",
14579
+ "listIgMedia"
14580
+ ],
14581
+ "inputJsonSchema": {
14582
+ "type": "object",
14583
+ "properties": {
14584
+ "pageId": {
14585
+ "type": "string",
14586
+ "minLength": 1
14587
+ }
14588
+ },
14589
+ "required": [
14590
+ "pageId"
14591
+ ],
14592
+ "additionalProperties": false,
14593
+ "$schema": "http://json-schema.org/draft-07/schema#"
14594
+ }
14595
+ },
14344
14596
  {
14345
14597
  "id": "listMedia",
14346
14598
  "domain": "core",
@@ -14429,6 +14681,33 @@ var CLI_MANIFEST = {
14429
14681
  "$schema": "http://json-schema.org/draft-07/schema#"
14430
14682
  }
14431
14683
  },
14684
+ {
14685
+ "id": "markReimbursementPaid",
14686
+ "domain": "creators",
14687
+ "description": "Record that a creator has been paid back for a meal they bought on a reimbursing board. This moves no money \u2014 it only writes down that the client already sent it by their own means, so never call it unless the client tells you the payment has actually gone out. The submission must be approved and its reimbursement still pending; a submission with no receipt was never on a reimbursing board and is rejected. Read the receipt total off creators.creatorContentSubmission before recording anything.",
14688
+ "type": "mutation",
14689
+ "path": [
14690
+ "api",
14691
+ "dfy",
14692
+ "markReimbursementPaid"
14693
+ ],
14694
+ "inputJsonSchema": {
14695
+ "type": "object",
14696
+ "properties": {
14697
+ "submissionId": {
14698
+ "type": "string"
14699
+ },
14700
+ "reimbursementPaidNote": {
14701
+ "type": "string"
14702
+ }
14703
+ },
14704
+ "required": [
14705
+ "submissionId"
14706
+ ],
14707
+ "additionalProperties": false,
14708
+ "$schema": "http://json-schema.org/draft-07/schema#"
14709
+ }
14710
+ },
14432
14711
  {
14433
14712
  "id": "planAds",
14434
14713
  "domain": "ads",
@@ -14568,7 +14847,7 @@ var CLI_MANIFEST = {
14568
14847
  {
14569
14848
  "id": "publishAds",
14570
14849
  "domain": "ads",
14571
- "description": "Publish a plan produced by planAds. Declare what should be recorded once the ads exist through effects, and the worker runs them as part of the job \u2014 a recruitment publish must pass linkRecruitmentOffer with its offerId and creativeIds, which stamps the creatives, stamps the offer that the monthly sourcing cap and the dashboard's spend both read, and texts the program's approver that sourcing is live; omitting it is refused rather than silently skipped. Doing it afterwards through a separate call is a step that can be missed, and missing it is silent. Pass back the variables, overrides and planHash that planAds returned, unchanged. The server re-derives the tree and refuses to publish if it no longer matches the hash, so re-plan and show the human the difference if that happens. This returns as soon as the work is queued \u2014 poll getJob with the returned jobId and jobType to follow it, and read the job's effect outcomes rather than assuming they ran. Everything is created paused; use setAdCampaignStatus to start it. Requires an explicit confirm.",
14850
+ "description": "Publish a plan produced by planAds. Declare what should be recorded once the ads exist through effects, and the worker runs them as part of the job \u2014 a recruitment publish must pass linkRecruitmentOffer with its offerId and creativeIds, which stamps the creatives, stamps the offer that the monthly sourcing cap and the dashboard's spend both read, and texts the program's approver that sourcing is live, and a directOffer publish must pass linkFeastCampaign with the campaignId it runs for, which is what puts its spend on the campaign's ads panel and KPIs; omitting either is refused rather than silently skipped. Doing it afterwards through a separate call is a step that can be missed, and missing it is silent. Pass back the variables, overrides and planHash that planAds returned, unchanged. The server re-derives the tree and refuses to publish if it no longer matches the hash, so re-plan and show the human the difference if that happens. This returns as soon as the work is queued \u2014 poll getJob with the returned jobId and jobType to follow it, and read the job's effect outcomes rather than assuming they ran. Everything is created paused; use setAdCampaignStatus to start it. Requires an explicit confirm.",
14572
14851
  "type": "mutation",
14573
14852
  "path": [
14574
14853
  "api",
@@ -16708,6 +16987,119 @@ Example - opted-in members with more than 5 visits, newest first:
16708
16987
  "resetProgress"
16709
16988
  ],
16710
16989
  "additionalProperties": false
16990
+ },
16991
+ {
16992
+ "type": "object",
16993
+ "properties": {
16994
+ "type": {
16995
+ "type": "string",
16996
+ "const": "setCustomProperty"
16997
+ },
16998
+ "setCustomProperty": {
16999
+ "type": "object",
17000
+ "properties": {
17001
+ "propertyId": {
17002
+ "type": "string"
17003
+ },
17004
+ "operation": {
17005
+ "anyOf": [
17006
+ {
17007
+ "type": "object",
17008
+ "properties": {
17009
+ "type": {
17010
+ "type": "string",
17011
+ "const": "set"
17012
+ },
17013
+ "set": {
17014
+ "type": "object",
17015
+ "properties": {
17016
+ "booleanValue": {
17017
+ "type": [
17018
+ "boolean",
17019
+ "null"
17020
+ ]
17021
+ },
17022
+ "isDateValue": {
17023
+ "anyOf": [
17024
+ {
17025
+ "type": "string",
17026
+ "format": "date-time"
17027
+ },
17028
+ {
17029
+ "type": "null"
17030
+ }
17031
+ ]
17032
+ },
17033
+ "localDayOfYear": {
17034
+ "type": [
17035
+ "string",
17036
+ "null"
17037
+ ]
17038
+ },
17039
+ "numberValue": {
17040
+ "type": [
17041
+ "number",
17042
+ "null"
17043
+ ]
17044
+ },
17045
+ "stringValue": {
17046
+ "type": [
17047
+ "string",
17048
+ "null"
17049
+ ]
17050
+ }
17051
+ },
17052
+ "additionalProperties": false
17053
+ }
17054
+ },
17055
+ "required": [
17056
+ "type",
17057
+ "set"
17058
+ ],
17059
+ "additionalProperties": false
17060
+ },
17061
+ {
17062
+ "type": "object",
17063
+ "properties": {
17064
+ "type": {
17065
+ "type": "string",
17066
+ "const": "increment"
17067
+ },
17068
+ "increment": {
17069
+ "type": "object",
17070
+ "properties": {
17071
+ "amount": {
17072
+ "type": "number"
17073
+ }
17074
+ },
17075
+ "required": [
17076
+ "amount"
17077
+ ],
17078
+ "additionalProperties": false,
17079
+ "description": "Number properties only. Added to the current value, or to 0."
17080
+ }
17081
+ },
17082
+ "required": [
17083
+ "type",
17084
+ "increment"
17085
+ ],
17086
+ "additionalProperties": false
17087
+ }
17088
+ ]
17089
+ }
17090
+ },
17091
+ "required": [
17092
+ "propertyId",
17093
+ "operation"
17094
+ ],
17095
+ "additionalProperties": false
17096
+ }
17097
+ },
17098
+ "required": [
17099
+ "type",
17100
+ "setCustomProperty"
17101
+ ],
17102
+ "additionalProperties": false
16711
17103
  }
16712
17104
  ]
16713
17105
  }
@@ -18427,6 +18819,119 @@ Example - opted-in members with more than 5 visits, newest first:
18427
18819
  "resetProgress"
18428
18820
  ],
18429
18821
  "additionalProperties": false
18822
+ },
18823
+ {
18824
+ "type": "object",
18825
+ "properties": {
18826
+ "type": {
18827
+ "type": "string",
18828
+ "const": "setCustomProperty"
18829
+ },
18830
+ "setCustomProperty": {
18831
+ "type": "object",
18832
+ "properties": {
18833
+ "propertyId": {
18834
+ "type": "string"
18835
+ },
18836
+ "operation": {
18837
+ "anyOf": [
18838
+ {
18839
+ "type": "object",
18840
+ "properties": {
18841
+ "type": {
18842
+ "type": "string",
18843
+ "const": "set"
18844
+ },
18845
+ "set": {
18846
+ "type": "object",
18847
+ "properties": {
18848
+ "booleanValue": {
18849
+ "type": [
18850
+ "boolean",
18851
+ "null"
18852
+ ]
18853
+ },
18854
+ "isDateValue": {
18855
+ "anyOf": [
18856
+ {
18857
+ "type": "string",
18858
+ "format": "date-time"
18859
+ },
18860
+ {
18861
+ "type": "null"
18862
+ }
18863
+ ]
18864
+ },
18865
+ "localDayOfYear": {
18866
+ "type": [
18867
+ "string",
18868
+ "null"
18869
+ ]
18870
+ },
18871
+ "numberValue": {
18872
+ "type": [
18873
+ "number",
18874
+ "null"
18875
+ ]
18876
+ },
18877
+ "stringValue": {
18878
+ "type": [
18879
+ "string",
18880
+ "null"
18881
+ ]
18882
+ }
18883
+ },
18884
+ "additionalProperties": false
18885
+ }
18886
+ },
18887
+ "required": [
18888
+ "type",
18889
+ "set"
18890
+ ],
18891
+ "additionalProperties": false
18892
+ },
18893
+ {
18894
+ "type": "object",
18895
+ "properties": {
18896
+ "type": {
18897
+ "type": "string",
18898
+ "const": "increment"
18899
+ },
18900
+ "increment": {
18901
+ "type": "object",
18902
+ "properties": {
18903
+ "amount": {
18904
+ "type": "number"
18905
+ }
18906
+ },
18907
+ "required": [
18908
+ "amount"
18909
+ ],
18910
+ "additionalProperties": false,
18911
+ "description": "Number properties only. Added to the current value, or to 0."
18912
+ }
18913
+ },
18914
+ "required": [
18915
+ "type",
18916
+ "increment"
18917
+ ],
18918
+ "additionalProperties": false
18919
+ }
18920
+ ]
18921
+ }
18922
+ },
18923
+ "required": [
18924
+ "propertyId",
18925
+ "operation"
18926
+ ],
18927
+ "additionalProperties": false
18928
+ }
18929
+ },
18930
+ "required": [
18931
+ "type",
18932
+ "setCustomProperty"
18933
+ ],
18934
+ "additionalProperties": false
18430
18935
  }
18431
18936
  ]
18432
18937
  }
@@ -22004,7 +22509,7 @@ Example - opted-in members with more than 5 visits, newest first:
22004
22509
  {
22005
22510
  "id": "updateInfluencerBoardConfig",
22006
22511
  "domain": "creators",
22007
- "description": "Create or update a location's creator program. This is an upsert \u2014 there is no separate create tool, and the config is keyed by locationId, one per location \u2014 so calling it for a location with no program creates one, seeding a 5000-cent dining credit and leaving landingPageConfirmed, calendarConfigured and passConfigured false. Omitted fields are left alone. schedulingMode gets no default, so set it on the first call or the Design creator program task stays incomplete: self_schedule_approval lets approved creators book themselves, apply_only collects applications for you to schedule. Note the launch check is looser than the task check \u2014 launchInfluencerCampaign only requires landingPageConfirmed and a positive credit, so a program can go live while its setup task still reads incomplete.",
22512
+ "description": "Create or update a location's creator program. This is an upsert \u2014 there is no separate create tool, and the config is keyed by locationId, one per location \u2014 so calling it for a location with no program creates one, seeding a 5000-cent dining credit and leaving landingPageConfirmed, calendarConfigured, passConfigured and reimbursementEnabled false. Omitted fields are left alone. reimbursementEnabled switches the board from comping the meal to reimbursing a meal the creator paid for, and foodCreditAmountCents becomes the reimbursement cap rather than a dining credit \u2014 it changes what creators are promised on the landing page, brief and rights agreement, so never set it without the client asking for it. schedulingMode gets no default, so set it on the first call or the Design creator program task stays incomplete: self_schedule_approval lets approved creators book themselves, apply_only collects applications for you to schedule. Note the launch check is looser than the task check \u2014 launchInfluencerCampaign only requires landingPageConfirmed and a positive credit, so a program can go live while its setup task still reads incomplete.",
22008
22513
  "type": "mutation",
22009
22514
  "path": [
22010
22515
  "api",
@@ -22043,9 +22548,6 @@ Example - opted-in members with more than 5 visits, newest first:
22043
22548
  "passConfigured": {
22044
22549
  "type": "boolean"
22045
22550
  },
22046
- "googleMapsLink": {
22047
- "type": "string"
22048
- },
22049
22551
  "maxBookingDaysOut": {
22050
22552
  "anyOf": [
22051
22553
  {
@@ -22099,7 +22601,8 @@ Example - opted-in members with more than 5 visits, newest first:
22099
22601
  {
22100
22602
  "type": "null"
22101
22603
  }
22102
- ]
22604
+ ],
22605
+ "description": "Despite the name, this covers the WHOLE visit, not just the run-up to it: anything creators must know or do before (download an app, book a reservation), during (check in with the host), or after (text a photo of the receipt). The text is injected verbatim into both the pre-visit and post-visit SMS agents' prompts, so write it as instructions to the creator and cover every stage in this one field \u2014 there is no separate post-visit instructions field."
22103
22606
  },
22104
22607
  "minFollowerCount": {
22105
22608
  "anyOf": [
@@ -22136,6 +22639,9 @@ Example - opted-in members with more than 5 visits, newest first:
22136
22639
  "type": "null"
22137
22640
  }
22138
22641
  ]
22642
+ },
22643
+ "reimbursementEnabled": {
22644
+ "type": "boolean"
22139
22645
  }
22140
22646
  },
22141
22647
  "required": [
@@ -23829,6 +24335,43 @@ Example - opted-in members with more than 5 visits, newest first:
23829
24335
  "lastVisit"
23830
24336
  ],
23831
24337
  "additionalProperties": false
24338
+ },
24339
+ {
24340
+ "type": "object",
24341
+ "properties": {
24342
+ "type": {
24343
+ "type": "string",
24344
+ "const": "customProperty"
24345
+ },
24346
+ "customProperty": {
24347
+ "type": "object",
24348
+ "properties": {
24349
+ "propertyId": {
24350
+ "type": "string"
24351
+ },
24352
+ "propertyType": {
24353
+ "type": "string",
24354
+ "enum": [
24355
+ "boolean",
24356
+ "dayOfYear",
24357
+ "date",
24358
+ "number",
24359
+ "string"
24360
+ ]
24361
+ }
24362
+ },
24363
+ "required": [
24364
+ "propertyId",
24365
+ "propertyType"
24366
+ ],
24367
+ "additionalProperties": false
24368
+ }
24369
+ },
24370
+ "required": [
24371
+ "type",
24372
+ "customProperty"
24373
+ ],
24374
+ "additionalProperties": false
23832
24375
  }
23833
24376
  ]
23834
24377
  }
package/feast/SKILL.md CHANGED
@@ -100,14 +100,19 @@ Many tasks are multi-step and have a required ordering the app normally enforces
100
100
  | Creating, cloning or configuring a campaign; promotions | `references/workflows/campaigns.md` |
101
101
  | Anything touching automations — creating, editing, simulating, promoting a draft | `references/workflows/automations.md` |
102
102
  | Editing funnel screens, applying a funnel template, staging a new screen | `references/workflows/funnels.md` |
103
- | Writing Meta ad copy — guest-facing or creator recruitment | `references/workflows/facebook.md` |
103
+ | Writing guest-facing Meta ad copy (`adCopy`) | `references/workflows/ad-copy-guest.md` |
104
+ | Writing creator-recruitment ad copy (`recruitmentAdCopy`) | `references/workflows/ad-copy-creator.md` |
104
105
  | Publishing, pausing, budgeting or diagnosing Meta ads | `references/workflows/ads.md` |
105
106
  | Creator sourcing — approving applicants, reviewing content, creatives, payouts | `references/workflows/creators.md` |
106
107
  | Members-program rewards; reading or saving the wallet pass configuration | `references/workflows/members-program.md` |
107
108
  | Working the onboarding taskboard; brand identity; phone, media, invites, billing | `references/workflows/onboarding.md` |
108
109
  | Searching guests/members; querying anything via the data catalog | `references/workflows/guests.md` |
109
110
 
110
- Read more than one when a task spans them — a new campaign usually means `campaigns.md` plus `automations.md` and `funnels.md`, and publishing a recruitment ad means `creators.md` plus `facebook.md` and `ads.md`.
111
+ Every row names one file, and one file is the whole answer for that row — pick the row that matches what you're doing and read only it. The two ad-copy rows are mutually exclusive: you are writing to guests or to creators, never both in one piece of copy.
112
+
113
+ Read more than one file when a task genuinely spans steps — a new campaign usually means `campaigns.md` plus `automations.md` and `funnels.md`. Read each one as you reach that step rather than gathering them up front: a file stays in context for the rest of the session, so one you open speculatively is re-read on every later turn.
114
+
115
+ When the ask is a question rather than a change — where something lives, what a field means, which link to send — read the single file the table names and answer from it.
111
116
 
112
117
  Some things are deliberately **not exposed**: replying to a guest or a creator by SMS, firing an automation at a live member, pass image generation, ad-copy generation (write it yourself), and publishing creator content as partnership ads. The workflow files say which. Don't fabricate a call for a workflow whose tools aren't listed by `feast tools` — tell the user that part isn't available yet.
113
118
 
@@ -0,0 +1,94 @@
1
+ # Creator-recruitment ad copy (`recruitmentAdCopy`)
2
+
3
+ > Part of the Feastalytics CLI workflows. Confirm a tool exists with `feast tools` before relying on it, and get its exact fields from `feast describe <tool>` — this file gives the *meaning* and *ordering* the schema can't.
4
+
5
+ This is the creator-facing half of Meta ad copy: `recruitmentAdCopy`, which sells a paid collaboration to a content creator shopping for brand deals.
6
+
7
+ **Writing to guests instead? Read `ad-copy-guest.md` and not this file.** `adCopy` sells the offer and the food to a hungry local scrolling past — a different audience and a completely different pitch. Conflating the two is the failure mode in this area, and it has happened repeatedly. A creator is not a customer; the food is their perk, not the pitch. If you catch yourself writing "claim your voucher" here, stop and start over.
8
+
9
+ For the creator program itself — applicants, visits, briefs, the decision loop — read `creators.md`. Publishing the finished ad is a separate job with its own loop — read `ads.md`.
10
+
11
+ **You write the copy yourself.** The dashboard has a "generate copy" button behind an LLM call; there is no CLI equivalent and you shouldn't want one, because it would be you calling an HTTP endpoint in order to call a model. The copy lands on a plain field of the campaign record, so saving it is trivial and covered at the bottom of this file. Everything between here and there is the part that's actually hard.
12
+
13
+ ## Variations: write a set, not a single
14
+
15
+ Meta's Advantage+ creative optimization tests combinations of headlines and primary texts against each other, so you're writing a *set* — several headlines and several primary texts that genuinely differ.
16
+
17
+ **Genuinely** is the load-bearing word. There is no required count. Three sharp variations that each take a real angle beat five where two are padding, and a set of near-identical rewrites teaches the optimizer nothing. Write as many as the collab actually supports. Judge it, and stop when the next variation would be filler.
18
+
19
+ The other reason to write more than one is that the restaurant may want to pick, and people form opinions by seeing alternatives rather than by being handed a single answer. So offering the set is usually the right move — but you have taste, and you should use it. Recommend the one you'd launch and say why. Cut the weak ones before anyone sees them rather than padding them in to look thorough. Three you'd defend beats five you wouldn't.
20
+
21
+ ## The audience
22
+
23
+ **Local food and lifestyle content creators** on Instagram and TikTok — someone scrolling for brand collabs, not a hungry person hunting a deal. The ad is decoupled from any consumer campaign the restaurant is running, *even if one is running right now*. Your job is to get the right creator to tap "Learn More" on a landing page that explains the collab in full — not to close the deal inside the ad.
24
+
25
+ ## Absolute rules — this is exactly where past generations went wrong
26
+
27
+ - **Never mention an offer, deal, voucher, promotion, discount, "claiming" anything, or pre-paying.** This is a collaboration, not a customer offer.
28
+ - **Don't pitch the food the way you'd pitch it to a diner.** The food is the perk; the collab is the pitch.
29
+ - **No customer-facing language** — "claim your voucher", "come hungry", "limited time offer", "this week only", "tap below to save".
30
+ - **Don't reuse the campaign's guest-facing framing** — banner copy, promotions, offer headlines. None of it belongs here, however good it is.
31
+ - **If there's a cash bonus, never imply it's automatic or guaranteed.** It is earned only if the restaurant selects the creator's reel to run as a paid ad. Phrasings like "earn a $100 bonus", "get a $100 bonus when you post", or "$100 bonus if you nail the brief" read as guaranteed-on-completion, and they have caused real creators to demand a bonus they hadn't earned. Always frame it conditionally: "a chance to earn", "up to", "if your reel gets picked to run as an ad".
32
+
33
+ ## What to pitch
34
+
35
+ - The restaurant is booking local food/lifestyle creators to come in, eat on the house, and post a short reel.
36
+ - **The creator gets:** a dining credit (order whatever they want), a creative brief with style direction but no script — they stay authentic — and, when acquisition is enabled, their reel boosted as a paid partnership ad alongside the restaurant's Instagram, which is free promotion to thousands of local foodies plus followers and engagement on their own page. Where a bonus exists, add it conditionally.
37
+ - **The creator gives:** one 30–60 second vertical reel (Instagram Reel / TikTok), filmed during the visit, submitted within 72 hours.
38
+ - **Eligibility:** an active food/lifestyle creator with a minimum local-area follower count on Instagram or TikTok.
39
+
40
+ ## Angles — rotate across the set
41
+
42
+ With a bonus: **get paid** ("Get paid to eat at X") → **free food + free promotion**, both sides of the exchange → **local creator call-out** ("Local foodies on IG — we want you") → **grow your page**, the boost and the new followers → **straightforward collab pitch**, no fluff: free meal + paid post + bonus.
43
+
44
+ Without a bonus, swap the first for **free food collab** ("Eat on us at X"), and grow-your-page for **brand partnership** (a collaboration, not a giveaway) or **behind-the-scenes** (be part of the restaurant's story).
45
+
46
+ Take as many of these as the collab genuinely supports rather than filling a quota.
47
+
48
+ ## Tone
49
+
50
+ Talk like a brand DM'ing a creator about a collab, not like a restaurant running an ad. Confident, peer-to-peer, slightly insider: "We're partnering with…", "We're booking creators for…", "Looking for local foodies who…".
51
+
52
+ Specifics over fluff — name the dollar amounts, the deliverable (one reel, 30–60s), the eligibility. Emoji fine in moderation (📸 🎥 🍴), don't spam. **Separate every sentence with a blank line (two newlines)** — each sentence has to stand alone visually, because a paragraph is a wall and a wall gets skipped.
53
+
54
+ **GOOD primary text:**
55
+
56
+ ```
57
+ Plum Vietnamese is booking local food creators this month. 📸
58
+
59
+ You get a $30 tab on us, a creative brief, and a chance to earn a $100 bonus if your reel gets run as a paid ad.
60
+
61
+ We'll also boost it as a partnership ad — free promo to thousands of local foodies. 1,000+ local IG/TikTok followers to apply.
62
+ ```
63
+
64
+ **GOOD headlines:** `Get paid to post about Plum` · `Local creators — eat free, post a reel` · `Foodies w/ 1,000+ followers, read this 👀`
65
+
66
+ **BAD — do not generate this:**
67
+
68
+ ```
69
+ Free meal at Plum Vietnamese this week! Claim your voucher and come hungry — you won't want to miss this deal. 🍴
70
+ ```
71
+
72
+ That's a customer offer wearing a creator ad's clothes. Every one of "free meal", "claim your voucher", "come hungry" and "this deal" is independently disqualifying. Note what the good version does instead: it names the restaurant as the one doing the booking, states the exchange in plain numbers, and gates on follower count — so the wrong reader self-selects out in the first line.
73
+
74
+ ## The terms are baked into the copy — record them
75
+
76
+ `foodCreditCents`, `creatorPayoutCents` and `minFollowerCount` on `recruitmentAdCopy` record the terms your copy actually stated. The dashboard compares them against the live creator board config and flags the copy as drifted when they diverge — so if you write "$30 tab" and leave them unset, nobody finds out when the credit later changes to $50 and the ad starts lying.
77
+
78
+ **Read the creator board config with `getInfluencerBoardConfig` first** — the dining credit, the bonus and the follower minimum live there and nowhere else. Write those exact numbers into the copy, and mirror them into these fields (in **cents** for the two money fields). Don't guess them, and don't ask the user for numbers the config already has.
79
+
80
+ The creator landing page is `/creator-landing` on the org's subdomain with `orgId`, `locId`, `campaignId` and UTM params — fiddly enough that you should reuse the existing `recruitmentAdCopy.landingPageUrl` when the campaign already has copy, rather than reconstructing it.
81
+
82
+ ## Saving it
83
+
84
+ One `updateCampaign` call writes `recruitmentAdCopy`. Run `feast describe updateCampaign` for the fields — alongside the headlines and primary texts it wants the landing page URL, the creative mix, a timestamp, and optional indices for the variation you're recommending.
85
+
86
+ **The one thing the schema won't tell you: `update.adCopy` replaces the whole object rather than merging into it.** Read the campaign with `getCampaign` first and send back everything you want kept, not just what changed.
87
+
88
+ ## Publishing
89
+
90
+ **CLI-drivable — read `ads.md`.** The loop is `listAdTemplates` → gather variables → `planAds` → `publishAds` (with its effects) → `getJob` → `setAdCampaignStatus`, and that file carries the ordering, the idempotency-key discipline, and the effect declarations that make the publish self-bookkeeping.
91
+
92
+ ---
93
+
94
+ > **Not exposed:** ad copy generation (write it yourself, per above). Read `references/links.md` before writing any dashboard link you hand over.
@@ -1,19 +1,14 @@
1
- # Meta ads
1
+ # Guest-facing ad copy (`adCopy`)
2
2
 
3
3
  > Part of the Feastalytics CLI workflows. Confirm a tool exists with `feast tools` before relying on it, and get its exact fields from `feast describe <tool>` — this file gives the *meaning* and *ordering* the schema can't.
4
4
 
5
- Two jobs live under Meta ads: **writing the ad copy** and **publishing the ad**. This file is the copywriting half; the publish loop (templates, planning, effects, activation) lives in `ads.md`.
5
+ This is the guest-facing half of Meta ad copy: `adCopy`, which sells the offer and the food to a hungry local scrolling past.
6
6
 
7
- **You write the copy yourself.** The dashboard has a "generate copy" button behind an LLM call; there is no CLI equivalent and you shouldn't want one, because it would be you calling an HTTP endpoint in order to call a model. The copy lands on a plain field of the campaign record, so saving it is trivial and covered at the bottom of this file. Everything between here and there is the part that's actually hard.
8
-
9
- ## First: which audience are you writing for?
10
-
11
- Two fields, two completely different pitches:
7
+ **Writing to creators instead? Read `ad-copy-creator.md` and not this file.** `recruitmentAdCopy` sells a paid collaboration to a content creator shopping for brand deals — a different audience and a completely different pitch. Conflating the two is the failure mode in this area, and it has happened repeatedly. A creator is not a customer; the food is their perk, not the pitch.
12
8
 
13
- - **`adCopy`** — guest-facing. Sells the offer and the food to a hungry local scrolling past.
14
- - **`recruitmentAdCopy`** — creator-facing. Sells a paid collaboration to a content creator shopping for brand deals.
9
+ Publishing the finished ad is a separate job with its own loop — read `ads.md` for that.
15
10
 
16
- **Conflating them is the failure mode in this area — it has happened repeatedly.** A creator is not a customer; the food is their perk, not the pitch. Decide which one you're writing before you write a word, and then read only that section below. If you catch yourself writing "claim your voucher" in a recruitment ad, stop and start over.
11
+ **You write the copy yourself.** The dashboard has a "generate copy" button behind an LLM call; there is no CLI equivalent and you shouldn't want one, because it would be you calling an HTTP endpoint in order to call a model. The copy lands on a plain field of the campaign record, so saving it is trivial and covered at the bottom of this file. Everything between here and there is the part that's actually hard.
17
12
 
18
13
  ## Variations: write a set, not a single
19
14
 
@@ -23,11 +18,7 @@ Meta's Advantage+ creative optimization tests combinations of headlines and prim
23
18
 
24
19
  The other reason to write more than one is that the restaurant may want to pick, and people form opinions by seeing alternatives rather than by being handed a single answer. So offering the set is usually the right move — but you have taste, and you should use it. Recommend the one you'd launch and say why. Cut the weak ones before anyone sees them rather than padding them in to look thorough. Three you'd defend beats five you wouldn't.
25
20
 
26
- ---
27
-
28
- ## Guest-facing copy (`adCopy`)
29
-
30
- ### Read before you write
21
+ ## Read before you write
31
22
 
32
23
  Generic copy is the failure mode, and specifics are the entire job. The difference between an ad that works and one that doesn't is almost never cleverness — it's whether the copy contains something only this restaurant could have said. So go get those things first:
33
24
 
@@ -40,7 +31,7 @@ The landing page URL is `https://{referrer}.feastalytics.com/campaign/{campaignI
40
31
 
41
32
  Mine all of it for things a human would actually remember: opening dates, the street, menu item names, sweepstakes mechanics, numbers, proper nouns. **If the source has real specifics and your copy says "taco time!", you did it wrong.** When you finish a draft, check that you couldn't paste it onto a different restaurant's campaign without anyone noticing.
42
33
 
43
- ### Headlines — each ≤ 40 characters
34
+ ## Headlines — each ≤ 40 characters
44
35
 
45
36
  The headline appears *below* the image or video. Forty characters is a hard ceiling; Meta truncates past it, and a headline that dies mid-word looks broken.
46
37
 
@@ -54,7 +45,7 @@ Each headline in your set should take a **different angle**. These five are the
54
45
 
55
46
  No generic marketing language. Write like a person, not a brand.
56
47
 
57
- ### Primary text — each 2–4 sentences
48
+ ## Primary text — each 2–4 sentences
58
49
 
59
50
  The primary text appears *above* the image or video. It's the first thing anyone reads, and it's read in a fast scroll on a phone.
60
51
 
@@ -65,7 +56,7 @@ The primary text appears *above* the image or video. It's the first thing anyone
65
56
  - Emoji are welcome when they add energy or visual punch. **At most one per sentence**, never forced. A well-placed emoji > no emoji > emoji spam.
66
57
  - Each variation takes a different approach, but all of them must work whether the viewer sees a static image or a video (see `creativeMix` below).
67
58
 
68
- ### Voice
59
+ ## Voice
69
60
 
70
61
  Write like you're texting a friend about a spot you're genuinely hyped about. Not like a brand's social media manager. Not like a restaurant's About page.
71
62
 
@@ -108,7 +99,7 @@ Claim your voucher and come see what the hype is about.
108
99
 
109
100
  Study what actually changes between them. The bad versions aren't wrong on the facts — they carry the same information. They fail because they *announce* where the good ones *react*. "We are giving away" is a press release; "Yeah, you read that right" is a person. The good versions also front-load the hook into the first line, break every sentence onto its own visual row, and trade a complete sentence for a fragment wherever the fragment hits harder. Notice too that neither good version is longer than the bad one it replaces — this is compression, not decoration.
110
101
 
111
- ### `creativeMix` changes what the copy may assume
102
+ ## `creativeMix` changes what the copy may assume
112
103
 
113
104
  Set this to what's actually true of the assets that will run, then write to it:
114
105
 
@@ -118,82 +109,15 @@ Set this to what's actually true of the assets that will run, then write to it:
118
109
 
119
110
  Copy that only makes sense next to a visible offer, running as `mixed`, will quietly underperform for half the audience.
120
111
 
121
- ### Video-led campaigns: the one thing you can't do
112
+ ## Video-led campaigns: the one thing you can't do
122
113
 
123
114
  The in-app generator **feeds the video assets to the model as multimodal input** and mines them for quotes, moments and on-screen specifics that end up in the copy. **You cannot watch a video from the CLI.**
124
115
 
125
116
  So for a `video_only` or `mixed` campaign, either work from a description or transcript the user gives you — saying plainly that's what you're working from — or write what you can from the landing page and tell the user the in-app dialog will do better here, because it can see the footage. Don't quietly produce video-campaign copy that never references the video and present it as equivalent. It isn't.
126
117
 
127
- ---
128
-
129
- ## Creator-recruitment copy (`recruitmentAdCopy`)
130
-
131
- **Read this section only when writing `recruitmentAdCopy`.**
132
-
133
- The audience is **local food and lifestyle content creators** on Instagram and TikTok — someone scrolling for brand collabs, not a hungry person hunting a deal. The ad is decoupled from any consumer campaign the restaurant is running, *even if one is running right now*. Your job is to get the right creator to tap "Learn More" on a landing page that explains the collab in full — not to close the deal inside the ad.
134
-
135
- ### Absolute rules — this is exactly where past generations went wrong
136
-
137
- - **Never mention an offer, deal, voucher, promotion, discount, "claiming" anything, or pre-paying.** This is a collaboration, not a customer offer.
138
- - **Don't pitch the food the way you'd pitch it to a diner.** The food is the perk; the collab is the pitch.
139
- - **No customer-facing language** — "claim your voucher", "come hungry", "limited time offer", "this week only", "tap below to save".
140
- - **Don't reuse the campaign's guest-facing framing** — banner copy, promotions, offer headlines. None of it belongs here, however good it is.
141
- - **If there's a cash bonus, never imply it's automatic or guaranteed.** It is earned only if the restaurant selects the creator's reel to run as a paid ad. Phrasings like "earn a $100 bonus", "get a $100 bonus when you post", or "$100 bonus if you nail the brief" read as guaranteed-on-completion, and they have caused real creators to demand a bonus they hadn't earned. Always frame it conditionally: "a chance to earn", "up to", "if your reel gets picked to run as an ad".
142
-
143
- ### What to pitch
144
-
145
- - The restaurant is booking local food/lifestyle creators to come in, eat on the house, and post a short reel.
146
- - **The creator gets:** a dining credit (order whatever they want), a creative brief with style direction but no script — they stay authentic — and, when acquisition is enabled, their reel boosted as a paid partnership ad alongside the restaurant's Instagram, which is free promotion to thousands of local foodies plus followers and engagement on their own page. Where a bonus exists, add it conditionally.
147
- - **The creator gives:** one 30–60 second vertical reel (Instagram Reel / TikTok), filmed during the visit, submitted within 72 hours.
148
- - **Eligibility:** an active food/lifestyle creator with a minimum local-area follower count on Instagram or TikTok.
149
-
150
- ### Angles — rotate across the set
151
-
152
- With a bonus: **get paid** ("Get paid to eat at X") → **free food + free promotion**, both sides of the exchange → **local creator call-out** ("Local foodies on IG — we want you") → **grow your page**, the boost and the new followers → **straightforward collab pitch**, no fluff: free meal + paid post + bonus.
153
-
154
- Without a bonus, swap the first for **free food collab** ("Eat on us at X"), and grow-your-page for **brand partnership** (a collaboration, not a giveaway) or **behind-the-scenes** (be part of the restaurant's story).
155
-
156
- As with guest copy, take as many of these as the collab genuinely supports rather than filling a quota.
157
-
158
- ### Tone
159
-
160
- Talk like a brand DM'ing a creator about a collab, not like a restaurant running an ad. Confident, peer-to-peer, slightly insider: "We're partnering with…", "We're booking creators for…", "Looking for local foodies who…".
161
-
162
- Specifics over fluff — name the dollar amounts, the deliverable (one reel, 30–60s), the eligibility. Emoji fine in moderation (📸 🎥 🍴), don't spam. The blank-line-between-sentences rule applies here too.
163
-
164
- **GOOD primary text:**
165
-
166
- ```
167
- Plum Vietnamese is booking local food creators this month. 📸
168
-
169
- You get a $30 tab on us, a creative brief, and a chance to earn a $100 bonus if your reel gets run as a paid ad.
170
-
171
- We'll also boost it as a partnership ad — free promo to thousands of local foodies. 1,000+ local IG/TikTok followers to apply.
172
- ```
173
-
174
- **GOOD headlines:** `Get paid to post about Plum` · `Local creators — eat free, post a reel` · `Foodies w/ 1,000+ followers, read this 👀`
175
-
176
- **BAD — do not generate this:**
177
-
178
- ```
179
- Free meal at Plum Vietnamese this week! Claim your voucher and come hungry — you won't want to miss this deal. 🍴
180
- ```
181
-
182
- That's a customer offer wearing a creator ad's clothes. Every one of "free meal", "claim your voucher", "come hungry" and "this deal" is independently disqualifying. Note what the good version does instead: it names the restaurant as the one doing the booking, states the exchange in plain numbers, and gates on follower count — so the wrong reader self-selects out in the first line.
183
-
184
- ### The terms are baked into the copy — record them
185
-
186
- `foodCreditCents`, `creatorPayoutCents` and `minFollowerCount` on `recruitmentAdCopy` record the terms your copy actually stated. The dashboard compares them against the live creator board config and flags the copy as drifted when they diverge — so if you write "$30 tab" and leave them unset, nobody finds out when the credit later changes to $50 and the ad starts lying.
187
-
188
- **Read the creator board config with `getInfluencerBoardConfig` first** — the dining credit, the bonus and the follower minimum live there and nowhere else. Write those exact numbers into the copy, and mirror them into these fields (in **cents** for the two money fields). Don't guess them, and don't ask the user for numbers the config already has.
189
-
190
- The creator landing page is `/creator-landing` on the org's subdomain with `orgId`, `locId`, `campaignId` and UTM params — fiddly enough that you should reuse the existing `recruitmentAdCopy.landingPageUrl` when the campaign already has copy, rather than reconstructing it.
191
-
192
- ---
193
-
194
118
  ## Saving it
195
119
 
196
- One `updateCampaign` call writes `adCopy` or `recruitmentAdCopy`. Run `feast describe updateCampaign` for the fields — alongside the headlines and primary texts it wants the landing page URL, the creative mix, a timestamp, and optional indices for the variation you're recommending.
120
+ One `updateCampaign` call writes `adCopy`. Run `feast describe updateCampaign` for the fields — alongside the headlines and primary texts it wants the landing page URL, the creative mix, a timestamp, and optional indices for the variation you're recommending.
197
121
 
198
122
  **The one thing the schema won't tell you: `update.adCopy` replaces the whole object rather than merging into it.** Read the campaign with `getCampaign` first and send back everything you want kept, not just what changed.
199
123
 
@@ -204,5 +128,3 @@ One `updateCampaign` call writes `adCopy` or `recruitmentAdCopy`. Run `feast des
204
128
  ---
205
129
 
206
130
  > **Not exposed:** ad copy generation (write it yourself, per above). Read `references/links.md` before writing any dashboard link you hand over.
207
-
208
- ---
@@ -4,7 +4,7 @@
4
4
 
5
5
  Publishing is CLI-drivable end to end: resolve a template, plan, publish, activate. The planner is the only door in — **never hand-assemble Meta campaign parameters**; `publishAds` re-derives everything from the template variables and refuses anything else.
6
6
 
7
- For the *words* in the ads, read `facebook.md` first — copywriting is its own discipline with its own failure modes.
7
+ For the *words* in the ads, read the copywriting file for your audience first — `ad-copy-guest.md` for guest-facing offer ads, `ad-copy-creator.md` for creator recruitment. Copywriting is its own discipline with its own failure modes.
8
8
 
9
9
  ### The model: template → plan → publish → activate
10
10
 
@@ -36,8 +36,8 @@ An effect that reports `error` in the job is a case for the dashboard, not for p
36
36
 
37
37
  ### Which template
38
38
 
39
- - **`directOffer`** — guest-facing offer ads for a campaign. Copy rules: the `adCopy` half of `facebook.md`.
40
- - **`recruitment`** — creator-recruitment ads. An always-on trickle with an enforced budget floor and ceiling. Creatives come from `createRecruitmentCreatives` → `listCreatives` (pass each creative's `imageKey` as a `libraryAsset` reference); copy rules: the `recruitmentAdCopy` half of `facebook.md`; program context: `creators.md`.
39
+ - **`directOffer`** — guest-facing offer ads for a campaign. Copy rules: `ad-copy-guest.md`.
40
+ - **`recruitment`** — creator-recruitment ads. An always-on trickle with an enforced budget floor and ceiling. Creatives come from `createRecruitmentCreatives` → `listCreatives` (pass each creative's `imageKey` as a `libraryAsset` reference); copy rules: `ad-copy-creator.md`; program context: `creators.md`.
41
41
  - **`addAds`** — add fresh creatives to an ad set that is already running. Copy the settings the new ads must match from an existing ad via `ads_get_ad_entities` — its description carries the exact field-by-field recipe, and Meta will happily publish a mismatched ad rather than reject it.
42
42
 
43
43
  ### Reading and steering what's live
@@ -47,4 +47,4 @@ An effect that reports `error` in the job is a case for the dashboard, not for p
47
47
  - `ads_activate_entity` — go-live for structures Feastalytics did *not* publish. No cascade: activate top-down and check `willDeliver`; a child under a paused parent is live in name only. For campaigns Feastalytics published, `setAdCampaignStatus` cascades and is the right tool.
48
48
  - `ads_get_datasets` / `ads_create_dataset` — pixel checks and creation. The pixel a campaign should optimise against is the one its funnel actually fires (from the layout config), not whichever pixel looks plausible on the account. After creating one, write its id back with `updateBrandIdentity` — creation alone connects nothing.
49
49
 
50
- > **Not exposed:** ad-copy generation (write it yourself — `facebook.md`), creative *content* editing on Meta (immutable there), and publishing creator content as partnership ads.
50
+ > **Not exposed:** ad-copy generation (write it yourself — `ad-copy-guest.md` / `ad-copy-creator.md`), creative *content* editing on Meta (immutable there), and publishing creator content as partnership ads.
@@ -57,7 +57,7 @@ The ads that bring applicants in are CLI-drivable end to end:
57
57
  1. `createRecruitmentCreatives` with the `campaignId` — it resolves (or creates) the campaign's recruitment offer itself, which is what groups the creatives and carries the monthly sourcing cap. Each run calls an image model per missing type; `force` deletes and regenerates the whole set, so don't pass it casually.
58
58
  2. `listCreatives` — each creative's `imageKey` is the reference `planAds` takes as a `libraryAsset`; `staleCreativeIds` flags creatives generated from an older version of their offer.
59
59
  3. Publish through the `recruitment` template in `ads.md`, declaring the **`linkRecruitmentOffer` effect** — the publish is refused without it. The effect stamps the creatives, links the offer (which the sourcing cap and dashboard spend read), and texts the program's approver that sourcing is live.
60
- 4. Copy rules for the ad live in `facebook.md` (`recruitmentAdCopy` — the creator-facing half; conflating it with guest copy is the classic failure).
60
+ 4. Copy rules for the ad live in `ad-copy-creator.md` (`recruitmentAdCopy`; conflating it with guest copy is the classic failure).
61
61
 
62
62
  ### The decision loop
63
63
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@feastalytics/cli",
3
- "version": "0.1.10",
3
+ "version": "0.1.11",
4
4
  "description": "Command-line client for the Feastalytics platform — list, create, and update campaigns, automations, offers, and members-program rewards from the terminal.",
5
5
  "type": "module",
6
6
  "bin": {