@feastalytics/cli 0.1.9 → 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
  }
@@ -13603,6 +13829,30 @@ var CLI_MANIFEST = {
13603
13829
  "$schema": "http://json-schema.org/draft-07/schema#"
13604
13830
  }
13605
13831
  },
13832
+ {
13833
+ "id": "getCreatorConversation",
13834
+ "domain": "creators",
13835
+ "description": "One creator's full SMS thread, newest first \u2014 what the dashboard's creator chat view renders. userId comes from listCreatorConversations, which is the queue; this is the read you make before summarizing an exchange or drafting a reply for the human to send, because the queue only carries the last message. Each row has the body, direction (from/to the creator's number), and timestamps. Returns empty when the creator has no phone number on file. Read-only: sending the reply and marking the thread read stay in the dashboard.",
13836
+ "type": "query",
13837
+ "path": [
13838
+ "api",
13839
+ "users",
13840
+ "getInfluencerMessages"
13841
+ ],
13842
+ "inputJsonSchema": {
13843
+ "type": "object",
13844
+ "properties": {
13845
+ "userId": {
13846
+ "type": "string"
13847
+ }
13848
+ },
13849
+ "required": [
13850
+ "userId"
13851
+ ],
13852
+ "additionalProperties": false,
13853
+ "$schema": "http://json-schema.org/draft-07/schema#"
13854
+ }
13855
+ },
13606
13856
  {
13607
13857
  "id": "getFunnelDraft",
13608
13858
  "domain": "funnel",
@@ -13723,6 +13973,66 @@ var CLI_MANIFEST = {
13723
13973
  "$schema": "http://json-schema.org/draft-07/schema#"
13724
13974
  }
13725
13975
  },
13976
+ {
13977
+ "id": "getMemberConversation",
13978
+ "domain": "membersProgram",
13979
+ "description": "One member's SMS thread and activity, newest first \u2014 what the dashboard's chat page renders, and the pair to searchUsers the way getCreatorConversation pairs with listCreatorConversations. serialNumber comes from searchUsers. eventTypes: ['sentText','receivedText'] is the conversation; adding scan, order, checkout, rewardAwarded or rewardRedeemed interleaves what happened between the messages. Unfiltered it fans out to every event source and returns the member's whole history unpaginated, so pass eventTypes unless you really want it all. Read-only: replying to a guest stays in the dashboard.",
13980
+ "type": "query",
13981
+ "path": [
13982
+ "api",
13983
+ "loyalty",
13984
+ "app",
13985
+ "listEventsForUser"
13986
+ ],
13987
+ "inputJsonSchema": {
13988
+ "type": "object",
13989
+ "properties": {
13990
+ "serialNumber": {
13991
+ "type": "string"
13992
+ },
13993
+ "eventTypes": {
13994
+ "type": "array",
13995
+ "items": {
13996
+ "type": "string",
13997
+ "enum": [
13998
+ "receivedText",
13999
+ "sentText",
14000
+ "customerInference",
14001
+ "scan",
14002
+ "passCreated",
14003
+ "passUpdated",
14004
+ "order",
14005
+ "rewardAwarded",
14006
+ "rewardRedeemed",
14007
+ "rewardExpiration",
14008
+ "offerExpired",
14009
+ "fbAttribution",
14010
+ "influencerAttribution",
14011
+ "tiktokAttribution",
14012
+ "googleAttribution",
14013
+ "miscAttribution",
14014
+ "referralAttribution",
14015
+ "passRegistered",
14016
+ "passDeleted",
14017
+ "checkout",
14018
+ "reservationCreated",
14019
+ "optOut",
14020
+ "subscriptionStarted",
14021
+ "subscriptionEnded",
14022
+ "subscriptionInvoice",
14023
+ "formSubmission",
14024
+ "autoReplyDraft"
14025
+ ]
14026
+ }
14027
+ }
14028
+ },
14029
+ "required": [
14030
+ "serialNumber"
14031
+ ],
14032
+ "additionalProperties": false,
14033
+ "$schema": "http://json-schema.org/draft-07/schema#"
14034
+ }
14035
+ },
13726
14036
  {
13727
14037
  "id": "getOnboardingForm",
13728
14038
  "domain": "core",
@@ -14142,7 +14452,7 @@ var CLI_MANIFEST = {
14142
14452
  {
14143
14453
  "id": "listCreatorConversations",
14144
14454
  "domain": "creators",
14145
- "description": "Every creator's SMS conversation with its unread state \u2014 the 'who is waiting on a reply' queue. `hasUnread` means their last message came in after ours and nobody has marked it read; those need a human. Each row carries the last message body, time and direction, the creator's handles, and `visitStatus`, a derived stage that is more reliable than reading raw columns off creatorVisitApplication. Read-only: replying to a creator and marking a conversation read both stay in the dashboard.",
14455
+ "description": "Every creator's SMS conversation with its unread state \u2014 the 'who is waiting on a reply' queue. `hasUnread` means their last message came in after ours and nobody has marked it read; those need a human. Each row carries the last message body, time and direction, the creator's handles, and `visitStatus`, a derived stage that is more reliable than reading raw columns off creatorVisitApplication. Read the full thread behind a row with getCreatorConversation and its userId. Read-only: replying to a creator and marking a conversation read both stay in the dashboard.",
14146
14456
  "type": "query",
14147
14457
  "path": [
14148
14458
  "api",
@@ -14257,6 +14567,32 @@ var CLI_MANIFEST = {
14257
14567
  "$schema": "http://json-schema.org/draft-07/schema#"
14258
14568
  }
14259
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
+ },
14260
14596
  {
14261
14597
  "id": "listMedia",
14262
14598
  "domain": "core",
@@ -14346,40 +14682,27 @@ var CLI_MANIFEST = {
14346
14682
  }
14347
14683
  },
14348
14684
  {
14349
- "id": "markCreativesPublished",
14685
+ "id": "markReimbursementPaid",
14350
14686
  "domain": "creators",
14351
- "description": "Record which Meta ad a recruitment creative was published in. The linkRecruitmentOffer effect on publishAds records this as part of the publish, so after an effect-declared publish there is nothing left to mark \u2014 call this only when ads were created outside publishAds or a publish's effect reported an error. Pass every creative that went into the ad \u2014 a dynamic ad rotates several at once, so they all take the same fbAdId. Without a record the creatives keep offering to be published again.",
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.",
14352
14688
  "type": "mutation",
14353
14689
  "path": [
14354
14690
  "api",
14355
14691
  "dfy",
14356
- "markCreativesPublished"
14692
+ "markReimbursementPaid"
14357
14693
  ],
14358
14694
  "inputJsonSchema": {
14359
14695
  "type": "object",
14360
14696
  "properties": {
14361
- "creatives": {
14362
- "type": "array",
14363
- "items": {
14364
- "type": "object",
14365
- "properties": {
14366
- "creativeId": {
14367
- "type": "string"
14368
- },
14369
- "fbAdId": {
14370
- "type": "string"
14371
- }
14372
- },
14373
- "required": [
14374
- "creativeId",
14375
- "fbAdId"
14376
- ],
14377
- "additionalProperties": false
14378
- }
14697
+ "submissionId": {
14698
+ "type": "string"
14699
+ },
14700
+ "reimbursementPaidNote": {
14701
+ "type": "string"
14379
14702
  }
14380
14703
  },
14381
14704
  "required": [
14382
- "creatives"
14705
+ "submissionId"
14383
14706
  ],
14384
14707
  "additionalProperties": false,
14385
14708
  "$schema": "http://json-schema.org/draft-07/schema#"
@@ -14524,7 +14847,7 @@ var CLI_MANIFEST = {
14524
14847
  {
14525
14848
  "id": "publishAds",
14526
14849
  "domain": "ads",
14527
- "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.",
14528
14851
  "type": "mutation",
14529
14852
  "path": [
14530
14853
  "api",
@@ -14992,7 +15315,7 @@ Example - opted-in members with more than 5 visits, newest first:
14992
15315
  {
14993
15316
  "id": "searchUsers",
14994
15317
  "domain": "membersProgram",
14995
- "description": "Search members (loyalty guests) and their activity. Returns a page of the most recent user events, one per member, each carrying the member's serialNumber plus the event type, time and related object. isUnread: true narrows the results to unread inbound texts only, overriding any broader eventTypes; progressMinBound/progressMaxBound bound the visit count. Paginate by passing the returned `cursor` back \u2014 an undefined cursor means no more pages.",
15318
+ "description": "Search members (loyalty guests) and their activity. Returns a page of the most recent user events, one per member, each carrying the member's serialNumber plus the event type, time and related object. isUnread: true narrows the results to unread inbound texts only, overriding any broader eventTypes; progressMinBound/progressMaxBound bound the visit count. Paginate by passing the returned `cursor` back \u2014 an undefined cursor means no more pages. This finds the member; getMemberConversation with their serialNumber loads their SMS thread or full timeline.",
14996
15319
  "type": "query",
14997
15320
  "path": [
14998
15321
  "api",
@@ -16664,6 +16987,119 @@ Example - opted-in members with more than 5 visits, newest first:
16664
16987
  "resetProgress"
16665
16988
  ],
16666
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
16667
17103
  }
16668
17104
  ]
16669
17105
  }
@@ -18383,6 +18819,119 @@ Example - opted-in members with more than 5 visits, newest first:
18383
18819
  "resetProgress"
18384
18820
  ],
18385
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
18386
18935
  }
18387
18936
  ]
18388
18937
  }
@@ -21960,7 +22509,7 @@ Example - opted-in members with more than 5 visits, newest first:
21960
22509
  {
21961
22510
  "id": "updateInfluencerBoardConfig",
21962
22511
  "domain": "creators",
21963
- "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.",
21964
22513
  "type": "mutation",
21965
22514
  "path": [
21966
22515
  "api",
@@ -21999,9 +22548,6 @@ Example - opted-in members with more than 5 visits, newest first:
21999
22548
  "passConfigured": {
22000
22549
  "type": "boolean"
22001
22550
  },
22002
- "googleMapsLink": {
22003
- "type": "string"
22004
- },
22005
22551
  "maxBookingDaysOut": {
22006
22552
  "anyOf": [
22007
22553
  {
@@ -22055,7 +22601,8 @@ Example - opted-in members with more than 5 visits, newest first:
22055
22601
  {
22056
22602
  "type": "null"
22057
22603
  }
22058
- ]
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."
22059
22606
  },
22060
22607
  "minFollowerCount": {
22061
22608
  "anyOf": [
@@ -22092,6 +22639,9 @@ Example - opted-in members with more than 5 visits, newest first:
22092
22639
  "type": "null"
22093
22640
  }
22094
22641
  ]
22642
+ },
22643
+ "reimbursementEnabled": {
22644
+ "type": "boolean"
22095
22645
  }
22096
22646
  },
22097
22647
  "required": [
@@ -23785,6 +24335,43 @@ Example - opted-in members with more than 5 visits, newest first:
23785
24335
  "lastVisit"
23786
24336
  ],
23787
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
23788
24375
  }
23789
24376
  ]
23790
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
 
@@ -42,7 +42,3 @@ A template-driven publish pipeline: `listAdTemplates` → `planAds` → `publish
42
42
  ## The data catalog
43
43
 
44
44
  `describeData` / `queryData` expose a read-only, org-scoped query surface over the data model — guests, orders, menu items, texts, creator visits, payouts. When no purpose-built tool answers a read question, the catalog usually does; `describeData` with no arguments is the index.
45
-
46
- ---
47
-
48
- Maintenance note: this file is currently hand-authored. The richest source of this domain knowledge is the in-app agent's prompt files (`src/agent-core/src/prompts/` — AutomationsPrompt, CampaignsPrompt, LayoutEnginePrompt, MembersProgramPrompt, OfferPrompt). A future improvement is to generate this reference from those, so the skill and the in-app agent never drift.
@@ -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
 
@@ -29,15 +29,15 @@ For the *words* in the ads, read `facebook.md` first — copywriting is its own
29
29
 
30
30
  Bookkeeping that must happen once the ads exist travels *inside* the publish as `effects`, and the worker runs it as part of the job — because a follow-up call you're supposed to remember is a follow-up call that gets missed, silently.
31
31
 
32
- - **A recruitment publish must declare `linkRecruitmentOffer`** with its `offerId` and `creativeIds` — the server refuses the publish without it. The effect stamps the creatives as published (no separate `markCreativesPublished` call needed), 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.
32
+ - **A recruitment publish must declare `linkRecruitmentOffer`** with its `offerId` and `creativeIds` — the server refuses the publish without it. The effect stamps the creatives as published, 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.
33
33
  - **`linkFeastCampaign`** records the published Meta campaign onto a Feast campaign, which is what makes its ads panel and KPIs see the spend.
34
34
 
35
- `markCreativesPublished` is the fallback for ads created outside `publishAds`, or for repairing a publish whose effect reported `error`.
35
+ An effect that reports `error` in the job is a case for the dashboard, not for patching around — surface it to the user.
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 @@ Bookkeeping that must happen once the ads exist travels *inside* the publish as
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,9 +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).
61
-
62
- `markCreativesPublished` is only the fallback for recording ads created outside `publishAds` or repairing an effect that reported `error`.
60
+ 4. Copy rules for the ad live in `ad-copy-creator.md` (`recruitmentAdCopy`; conflating it with guest copy is the classic failure).
63
61
 
64
62
  ### The decision loop
65
63
 
@@ -79,7 +77,9 @@ The ads that bring applicants in are CLI-drivable end to end:
79
77
 
80
78
  `listCreatorConversations` is the "who is waiting on a reply" queue: every creator's SMS thread with `hasUnread`, the last message body and direction, and a derived `visitStatus` chip that's more reliable than reading raw columns.
81
79
 
82
- **You cannot reply from the CLI, and you cannot clear the unread flag.** Both stay in the dashboard — texting a creator back is the highest-consequence action in this area. Surface who's waiting and what they said, then hand the user the conversation.
80
+ `getCreatorConversation` with a row's `userId` loads the full thread behind it, newest first. Read it before characterizing an exchange or drafting a reply for the human — the queue's last-message snippet is not enough context to speak for a whole conversation.
81
+
82
+ **You cannot reply from the CLI, and you cannot clear the unread flag.** Both stay in the dashboard — texting a creator back is the highest-consequence action in this area. Surface who's waiting, read the thread, propose the reply if asked, then hand the user the conversation to send it.
83
83
 
84
84
  ### Everything else: queryData
85
85
 
@@ -7,6 +7,8 @@
7
7
  - Filter with `query` (free-text name), `eventTypes` (e.g. `sentText`, `receivedText`, `scan`, `order`, `rewardAwarded`, `rewardRedeemed`, `checkout`, the `*Attribution` types), `progressMinBound`/`progressMaxBound` (visit-count range), `isUnread: true` (members with unanswered inbound texts), `orderBy` (ASC|DESC by event time).
8
8
  - Paginate with `limit` (default 100) and `cursor` (pass back the `cursor` from the previous call; an undefined cursor means no more pages).
9
9
 
10
+ `getMemberConversation` with a member's `serialNumber` loads their thread, newest first — the pair to `searchUsers` the same way `getCreatorConversation` pairs with `listCreatorConversations`. Always pass `eventTypes`: `["sentText","receivedText"]` is the SMS thread, and adding `scan`/`order`/`checkout`/`rewardAwarded`/`rewardRedeemed` interleaves what happened between the messages. Unfiltered it returns the member's entire history unpaginated.
11
+
10
12
  **Replying by SMS is NOT exposed, deliberately.** The send primitive enforces opt-out, quiet-hours, dedup, and rate limits *downstream* (not at the endpoint), and opt-in is currently gated only by a UI control. If a reply capability is ever exposed, it must run with confirmation and must not bypass those guardrails. For now, tell the user that replying to guests is done in the app.
11
13
 
12
14
  ## Everything else: the data catalog
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@feastalytics/cli",
3
- "version": "0.1.9",
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": {