@feastalytics/cli 0.1.6 → 0.1.7
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 +4950 -2687
- package/feast/SKILL.md +6 -3
- package/feast/references/domains.md +2 -2
- package/feast/references/links.md +1 -1
- package/feast/references/workflows/campaigns.md +4 -4
- package/feast/references/workflows/creators.md +36 -1
- package/feast/references/workflows/facebook.md +208 -0
- package/feast/references/workflows/funnels.md +1 -1
- package/feast/references/workflows/members-program.md +2 -2
- package/package.json +1 -1
package/feast/SKILL.md
CHANGED
|
@@ -73,11 +73,13 @@ Pass the target org with `--org <organizationId>`:
|
|
|
73
73
|
|
|
74
74
|
## Reads vs. writes
|
|
75
75
|
|
|
76
|
-
Query tools (listing, describing, reading) are safe and read-only. Mutation tools (create, update, clone, delete, apply) change production data
|
|
76
|
+
Query tools (listing, describing, reading) are safe and read-only. Mutation tools (create, update, clone, delete, apply) change production data.
|
|
77
77
|
|
|
78
78
|
- Mutations require `--org` explicitly.
|
|
79
|
-
- Before running, the CLI
|
|
80
|
-
-
|
|
79
|
+
- Before running one, the CLI resolves the organization server-side and prints its name, so a wrong `--org` shows up as the wrong restaurant rather than an opaque id. Read that line.
|
|
80
|
+
- **There is no confirmation prompt.** A mutation runs the moment you call it. Nothing asks twice, and nothing undoes it.
|
|
81
|
+
|
|
82
|
+
That last point matters most for the tools that reach the real world rather than just the database. Buying a phone number bills the account. Approving a creator visit or deciding a submission sends that person a text immediately and cannot be recalled. Publishing a campaign puts it live, and pricing a recurring promotion creates real Stripe products. Saving automation edits changes what guests receive. Treat those as irreversible, and get the user's intent straight *before* the call, because there is no gate after it.
|
|
81
83
|
|
|
82
84
|
Prefer reading before writing: e.g. `listCampaigns` to find the right `campaignId` before `updateCampaign`, or `describe`/`listAutomationFlows` before creating a flow.
|
|
83
85
|
|
|
@@ -98,6 +100,7 @@ Many tasks are multi-step and have a required ordering the app normally enforces
|
|
|
98
100
|
| Creating, cloning or configuring a campaign; offers in the strategy backlog | `references/workflows/campaigns.md` |
|
|
99
101
|
| Anything touching automations — creating, editing, simulating, promoting a draft | `references/workflows/automations.md` |
|
|
100
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` |
|
|
101
104
|
| Creator sourcing — approving applicants, reviewing their content, conversations | `references/workflows/creators.md` |
|
|
102
105
|
| Members-program rewards; reading or saving the wallet pass configuration | `references/workflows/members-program.md` |
|
|
103
106
|
| Working the onboarding taskboard; brand identity | `references/workflows/onboarding.md` |
|
|
@@ -4,7 +4,7 @@ Background for constructing tool input correctly. This is the conceptual map; th
|
|
|
4
4
|
|
|
5
5
|
## Organizations
|
|
6
6
|
|
|
7
|
-
The top-level tenant. Nearly every tool is scoped to one organization via `--org`. An org has one or more POS locations (Toast/Square/Clover); many tools that operate on menus or offers need a `locationId`, which you get from `
|
|
7
|
+
The top-level tenant. Nearly every tool is scoped to one organization via `--org`. An org has one or more POS locations (Toast/Square/Clover); many tools that operate on menus or offers need a `locationId`, which you get from `getOrganization` (it returns the org's `locations`) — not the organization id.
|
|
8
8
|
|
|
9
9
|
## Campaigns (acquisition)
|
|
10
10
|
|
|
@@ -21,7 +21,7 @@ Typical flow: `createCampaign` (set `isCreating: true` if you'll finish it with
|
|
|
21
21
|
- A **flow** is a named grouping of automations. A flow belongs to *either* a campaign *or* the members program (never both).
|
|
22
22
|
- `listAutomationFlows` scopes with input: `{ campaignId }` returns that campaign's flows; `{ scope: "membersProgram" }` returns members-program flows (those with no campaign). `listAutomations` returns every automation in the org, ordered by execution priority.
|
|
23
23
|
- **Authoring:** `createAutomationFlow` makes a flow; `batchEditAutomations` creates/updates/deletes automations in one atomic batch (create ops **require** a `flowId`); `updateAutomationFlow` / `deleteAutomationFlow` manage the flow itself; `simulateAutomations` dry-runs a flow with no real sends. See `workflows/automations.md` for the ordering and the trigger/condition/send-time rules.
|
|
24
|
-
- Templates: `listAutomationTemplates` → `
|
|
24
|
+
- Templates: `listAutomationTemplates` → `listTemplateAutomations` (preview) → `applyAutomationTemplate`. Only apply a template to a campaign/members-program that has no existing flows.
|
|
25
25
|
|
|
26
26
|
## Offers (DFY strategy)
|
|
27
27
|
|
|
@@ -61,7 +61,7 @@ The guest-facing site lives on the organization's own subdomain, `https://<subdo
|
|
|
61
61
|
/preview/<draftId>/<campaignId> the same draft, scoped to one campaign
|
|
62
62
|
```
|
|
63
63
|
|
|
64
|
-
Get `<subdomain>` from `
|
|
64
|
+
Get `<subdomain>` from `getOrganization` → `organization.subdomains2[].subdomain`. When the link is about a campaign, pick the subdomain matching that campaign's `referrers` rather than the first one. The funnel draft tools (`createFunnelDraft`, `getFunnelDraft`) already return the draft's `referrer`, so use that instead of looking it up again.
|
|
65
65
|
|
|
66
66
|
The `/preview/<draftId>` route is the payoff of the draft → preview → promote loop in `workflows/funnels.md`: it renders every screen of the staged funnel as a tree, so it's the right link to hand over after `stageFunnelEdit` and before `saveFunnelEdits`. It stops working once the draft is discarded or expires.
|
|
67
67
|
|
|
@@ -7,14 +7,14 @@
|
|
|
7
7
|
|
|
8
8
|
Fully doable via the CLI. The server does the heavy lifting (id generation, default config, the funnel prerequisite) — you sequence the calls.
|
|
9
9
|
|
|
10
|
-
1. `
|
|
10
|
+
1. `getOrganization` — read the org to get valid **referrers** (subdomains, from `subdomains2[].subdomain`) and location ids.
|
|
11
11
|
2. `createCampaign` with `{ "campaign": { "name": "...", "isCreating": true, "fbCampaigns": [], "attributionRules": [] } }` — keep the returned campaign **id** (a UUID). It comes back `isCreating: true`.
|
|
12
12
|
3. `populateCampaign` with the `campaignId` and a **`funnelType`**:
|
|
13
13
|
- `"reservation"` — no extra config.
|
|
14
14
|
- `"simpleRewards"` — needs `simpleRewardsConfig` with a `promotionName` and an image. Pass a public `imageUrl` string (the CLI can't do the app's file-upload path).
|
|
15
15
|
- `"prepay"` — needs `prepayConfig` with `promotionName`, `price`, and an image (`imageUrl`).
|
|
16
|
-
4. (optional) `
|
|
17
|
-
5. (optional) `applyAutomationTemplate` — the **retention** half: the follow-up messaging. Provisions the campaign's flow *and* its automations in one call, so you don't hand-build a flow for this path. Preview options first with `listAutomationTemplates` / `
|
|
16
|
+
4. (optional) `applyFunnelTemplate` — the **acquisition** half: the funnel screens a guest sees. Requires a fresh campaign whose funnel is unset; resolves the referrer from the campaign.
|
|
17
|
+
5. (optional) `applyAutomationTemplate` — the **retention** half: the follow-up messaging. Provisions the campaign's flow *and* its automations in one call, so you don't hand-build a flow for this path. Preview options first with `listAutomationTemplates` / `listTemplateAutomations`.
|
|
18
18
|
|
|
19
19
|
Steps 4 and 5 are the two independent halves of a working campaign — the funnel (what the guest sees) and the automations (the messaging that follows). A fully working campaign has a funnel with no screen errors and at least one automation flow.
|
|
20
20
|
|
|
@@ -30,7 +30,7 @@ The one-shot text→campaign endpoints (`createWithOffer` / `parseCampaignDescri
|
|
|
30
30
|
|
|
31
31
|
Fully doable from the CLI. Offers live in the organization's strategy backlog (the offer queue), sourced from real menu data.
|
|
32
32
|
|
|
33
|
-
1. `
|
|
33
|
+
1. `getOrganization` → get the **`locationId`** (from `locations`) — offer tools key on the location, **never** the organizationId.
|
|
34
34
|
2. `dfyGetMenuHierarchy` with that `locationId` — browse real menu items and prices.
|
|
35
35
|
3. `dfyListOffers` — see the current backlog; avoid duplicates.
|
|
36
36
|
4. `dfyCreateOffer` — create it. `dfyUpdateOffer` / `dfyDeleteOffer` to revise.
|
|
@@ -15,6 +15,41 @@ There is no separate application object. One row covers a creator's whole journe
|
|
|
15
15
|
|
|
16
16
|
**Gotcha:** denying an application stamps `postVisitFollowUpSentAt` and every content follow-up with the current time, as the way to suppress the remaining message sequence. So a denied row looks *completed* on those timestamps. Always pair a timestamp check with `approvalStatus == "approved"`.
|
|
17
17
|
|
|
18
|
+
### Setting up the program
|
|
19
|
+
|
|
20
|
+
The program lives on a **location**, not the organization — one config per `locationId`, which you get from `queryData interface.location`.
|
|
21
|
+
|
|
22
|
+
`updateInfluencerBoardConfig` is an **upsert**. There is no create tool: call it for a location with no program and it writes one, seeding a 5000-cent dining credit and leaving `landingPageConfirmed`, `calendarConfigured` and `passConfigured` false. Omitted fields are left alone on subsequent calls.
|
|
23
|
+
|
|
24
|
+
**Set `schedulingMode` on the first call.** It's the one field with no default, and without it the *Design creator program* task never completes no matter what else you fill in. `self_schedule_approval` lets approved creators book themselves; `apply_only` collects applications for the restaurant to schedule.
|
|
25
|
+
|
|
26
|
+
**The setup task and the launch check disagree.** The task wants `schedulingMode` *and* a positive credit *and* `landingPageConfirmed`. Launching only checks `landingPageConfirmed` and a positive credit — and since the credit's floor and its default are both 5000, that leaves `landingPageConfirmed` as the only real precondition. A program can be live while its task still reads incomplete; don't report the task as the launch gate.
|
|
27
|
+
|
|
28
|
+
`getInfluencerBoardConfig` returns the config (or `null`) plus the location's recruitment offers. **Read it before writing recruitment copy** — the dining credit, creator bonus and follower minimum you're supposed to quote live here and nowhere else. It's also how you check the bonus is non-zero before calling `decideCreatorSubmission` with `approvalType: "ad"`.
|
|
29
|
+
|
|
30
|
+
### Booking windows
|
|
31
|
+
|
|
32
|
+
`listAvailability` (no arguments, **org-wide** — filter by `locationId` or `campaignId` yourself), `createAvailability`, `updateAvailability`, `deleteAvailability`.
|
|
33
|
+
|
|
34
|
+
A window's `block` is one of two shapes: `once`, with a `utcStart` and `utcEnd`; or `weekly`, with start and end hour/minute, the `utcDaysOfWeek` it repeats on, and `blockUtcStart` for when the repetition begins.
|
|
35
|
+
|
|
36
|
+
**Everything is UTC and the restaurant will describe it in local time.** For weekly blocks `utcDaysOfWeek` is the day of week *in UTC*, so an evening local window that crosses midnight UTC lands on the **following** day — 9pm Friday New York is 02:00 Saturday UTC, and writing `Friday` there opens the wrong night. Convert the day and the time together, never just the time. This fails silently: you get a valid window on a day nobody asked for.
|
|
37
|
+
|
|
38
|
+
**Set `campaignId`, not just `locationId`.** It's optional in the schema and required by the task — *Set booking windows* completes only when a window carries the first campaign's id. Without it the window books fine and the task stays open forever.
|
|
39
|
+
|
|
40
|
+
`updateAvailability` replaces `block` whole rather than merging it, so send the complete block including the parts you aren't changing, and it returns nothing — re-read with `listAvailability` to confirm. `deleteAvailability` **succeeds silently on an id that doesn't exist**, so no error is not proof anything was removed; take ids from `listAvailability`. Deleting closes future slots but does not cancel visits already booked inside the window — those are separate rows.
|
|
41
|
+
|
|
42
|
+
### The creative brief
|
|
43
|
+
|
|
44
|
+
`createCreativeStrategy` has two paths behind one tool, and only one of them finishes synchronously:
|
|
45
|
+
|
|
46
|
+
- **`awareness`** — assembled from a fixed template and saved before the call returns. `generationStatus` comes back `complete`.
|
|
47
|
+
- **`cta`** — handed to a background LLM. You get a `strategyId` and `generationStatus: "generating"` immediately. **Poll `getCreativeStrategy` until it reads `complete` or `failed`** before using the brief or quoting anything from it.
|
|
48
|
+
|
|
49
|
+
`getCreativeStrategy` is the one tool that takes `organizationId` in its input rather than from `--org` — it also serves the creator-facing brief pages. Pass the organization you're acting on.
|
|
50
|
+
|
|
51
|
+
`updateCreativeStrategy` is the revision step. Two things to get right: omitting `strategyId` **creates a new strategy** instead of editing the one you meant, and it replaces the fields you send rather than merging them — so read first, apply your edits to the full `concepts` array, and send the whole thing back. Generating into a strategy that isn't still a draft is rejected rather than silently overwritten.
|
|
52
|
+
|
|
18
53
|
### The decision loop
|
|
19
54
|
|
|
20
55
|
1. `listCreatorApplications` — the approval queue, newest first, across every location. Takes no arguments. Use this rather than querying the data model: it carries **`instagramFollowerCount`**, which is usually the deciding factor and isn't reachable any other way.
|
|
@@ -33,6 +68,6 @@ There is no separate application object. One row covers a creator's whole journe
|
|
|
33
68
|
|
|
34
69
|
The `creators` schema exposes `creator` (the person, one row shared across all their applications) and `creatorVisitApplication` (one application/visit). Join on `creator.influencerId = creatorVisitApplication.userId`. Use it for anything the tools above don't answer — no-shows, per-location counts, repeat creators. Content submissions and payouts are **not** in the catalog.
|
|
35
70
|
|
|
36
|
-
> **Not exposed:** launching recruitment ads, scheduling or cancelling a visit, marking a visit attended, paying a bonus, and publishing creator content to Facebook. Those stay in the app.
|
|
71
|
+
> **Not exposed:** launching the program, launching recruitment ads, scheduling or cancelling a visit, marking a visit attended, paying a bonus, and publishing creator content to Facebook. Those stay in the app — so you can configure a program, set its booking windows and write its brief from here, but a human still has to launch it.
|
|
37
72
|
|
|
38
73
|
---
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# Meta ads
|
|
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
|
+
Two jobs live under Meta ads: **writing the ad copy** and **publishing the ad**. Only the first is CLI work today.
|
|
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:
|
|
12
|
+
|
|
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.
|
|
15
|
+
|
|
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.
|
|
17
|
+
|
|
18
|
+
## Variations: write a set, not a single
|
|
19
|
+
|
|
20
|
+
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.
|
|
21
|
+
|
|
22
|
+
**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 campaign actually supports: a rich offer with a strong landing page and a distinctive neighbourhood might carry five; a thin one-line promo might only honestly carry three. Judge it, and stop when the next variation would be filler.
|
|
23
|
+
|
|
24
|
+
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
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Guest-facing copy (`adCopy`)
|
|
29
|
+
|
|
30
|
+
### Read before you write
|
|
31
|
+
|
|
32
|
+
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
|
+
|
|
34
|
+
1. `getOrganization` — brand name, cuisine, and the subdomains in `subdomains2[].subdomain`.
|
|
35
|
+
2. `getCampaign` — `name`, `description`, `bannerConfig`, `promotions`, `referrers`, `shorthand`.
|
|
36
|
+
3. `listFunnelScreens` `{ "referrer": "<subdomain>", "campaignId": "<id>" }` — **the actual landing-page copy the guest sees after the click.** This is your source of truth for congruence: the trip from ad to landing page should feel like one continuous thing, not a bait-and-switch. Copy that promises something the landing page doesn't deliver burns the click.
|
|
37
|
+
4. `dfyListOffers` / `dfyGetMenuHierarchy` — real item names and real prices, not approximations of them.
|
|
38
|
+
|
|
39
|
+
The landing page URL is `https://{referrer}.feastalytics.com/campaign/{campaignId}`, using a referrer from the campaign's own `referrers` rather than just the org's first subdomain.
|
|
40
|
+
|
|
41
|
+
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
|
+
|
|
43
|
+
### Headlines — each ≤ 40 characters
|
|
44
|
+
|
|
45
|
+
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
|
+
|
|
47
|
+
Each headline in your set should take a **different angle**. These five are the ones that work for local restaurants — a menu to pick from, not a checklist to complete:
|
|
48
|
+
|
|
49
|
+
1. **Value** — lead with what they get: the offer, the free item, the deal. The safest angle and usually the strongest, because it answers "what's in it for me" before anyone has to think.
|
|
50
|
+
2. **Curiosity** — make them need to find out ("This spot on Fillmore is hiding something…"). Works only when there's a real answer waiting on the landing page; curiosity with nothing behind it reads as clickbait.
|
|
51
|
+
3. **Social proof** — popularity or local reputation ("The neighborhood's worst-kept secret"). Borrows credibility the restaurant already earned.
|
|
52
|
+
4. **Urgency** — time pressure or scarcity ("This week only", "Limited spots"). Only when it's true. Manufactured urgency on an evergreen offer is the fastest way to sound like every other ad in the feed.
|
|
53
|
+
5. **Locality** — the neighborhood, the street, the local identity. The one angle a national chain can't copy, and often the most distinctive thing available to you.
|
|
54
|
+
|
|
55
|
+
No generic marketing language. Write like a person, not a brand.
|
|
56
|
+
|
|
57
|
+
### Primary text — each 2–4 sentences
|
|
58
|
+
|
|
59
|
+
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
|
+
|
|
61
|
+
**The formatting rule that matters most: separate every sentence with a blank line (two newlines).** Each sentence has to stand alone visually. A paragraph is a wall; a wall gets skipped. This is not optional polish, and it is the single most-ignored rule in this file — check for it explicitly before you save anything.
|
|
62
|
+
|
|
63
|
+
- No hashtags.
|
|
64
|
+
- No "click the link below" / "tap below" — Meta owns the CTA button, and pointing at a link that isn't there is just confusing.
|
|
65
|
+
- 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
|
+
- 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
|
+
|
|
68
|
+
### Voice
|
|
69
|
+
|
|
70
|
+
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
|
+
|
|
72
|
+
- Short punchy fragments > grammatically perfect sentences.
|
|
73
|
+
- Confidence and excitement > polite and formal.
|
|
74
|
+
- Specific details > vague claims ("crispy baguette with savory fillings" > "delicious food").
|
|
75
|
+
- Talk **to** the reader, not **at** them.
|
|
76
|
+
|
|
77
|
+
**BAD** (robotic, corporate, flat):
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
We are giving away a free Coconut Matcha or Sea Salt Coffee with any regular nine inch banh mi. Our sandwiches are made fresh daily with crispy baguettes and savory fillings. Get your voucher and come hungry.
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
**GOOD** (energetic, specific, scroll-stopping):
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
Free Coconut Matcha with any banh mi. 🍵
|
|
87
|
+
|
|
88
|
+
Yeah, you read that right.
|
|
89
|
+
|
|
90
|
+
Crispy baguette, savory fillings, and a specialty coffee on the house. Grab your voucher before this one's gone.
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
**BAD:**
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
Our specialty coffees are the perfect sweet treat to balance a savory meal. Right now you can get one completely free when you order a regular banh mi.
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
**GOOD:**
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
Crispy baguette + savory fillings + a free specialty coffee? 👏
|
|
103
|
+
|
|
104
|
+
That's lunch sorted.
|
|
105
|
+
|
|
106
|
+
Claim your voucher and come see what the hype is about.
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
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
|
+
|
|
111
|
+
### `creativeMix` changes what the copy may assume
|
|
112
|
+
|
|
113
|
+
Set this to what's actually true of the assets that will run, then write to it:
|
|
114
|
+
|
|
115
|
+
- **`static_only`** — the offer is printed on the image. Reference it directly; the viewer always sees it.
|
|
116
|
+
- **`video_only`** — videos are awareness-driven and **do not show the offer on screen**. The copy has to stand up with no offer visible: intrigue, the restaurant, the experience.
|
|
117
|
+
- **`mixed`** — the hard case. Meta shows some viewers a video with no offer and others a static with the offer front and centre. The copy must read correctly **both** ways, which usually means naming the offer in words rather than gesturing at it ("free matcha with any banh mi", not "check out the deal above").
|
|
118
|
+
|
|
119
|
+
Copy that only makes sense next to a visible offer, running as `mixed`, will quietly underperform for half the audience.
|
|
120
|
+
|
|
121
|
+
### Video-led campaigns: the one thing you can't do
|
|
122
|
+
|
|
123
|
+
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
|
+
|
|
125
|
+
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
|
+
|
|
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
|
+
**You can't read the creator board config from the CLI.** Ask the user for the dining credit, the bonus and the follower minimum, write those exact numbers into the copy, and mirror them into these fields (in **cents** for the two money fields). Don't guess them.
|
|
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
|
+
## Saving it
|
|
195
|
+
|
|
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.
|
|
197
|
+
|
|
198
|
+
**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
|
+
|
|
200
|
+
## Publishing
|
|
201
|
+
|
|
202
|
+
**Not yet possible from the CLI.** Creating the ad in Meta — ad account, page, budget, targeting, creative — stays in the dashboard for now. Write the copy, then hand the user the campaign's `ads` panel (or `creative-strategy` for recruitment) to publish it.
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
> **Not exposed:** ad copy generation (write it yourself, per above), marking copy as pushed to Meta, and everything to do with creating, editing or pausing a live Meta ad. Read `references/links.md` before writing the dashboard link you hand over.
|
|
207
|
+
|
|
208
|
+
---
|
|
@@ -2,7 +2,7 @@
|
|
|
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
|
-
**Applying a funnel template** expands a whole screen tree server-side in one call: `
|
|
5
|
+
**Applying a funnel template** expands a whole screen tree server-side in one call: `applyFunnelTemplate` (needs the campaign's funnel unset — a fresh campaign — and resolves the referrer from the campaign). `deleteFunnel` tears one down.
|
|
6
6
|
|
|
7
7
|
**Editing individual funnel screens is now CLI-drivable** through a **draft → preview → promote** loop. You never apply edits locally: you stage them on an off-prod draft, preview the result at a stable URL, then save. Tools: `listFunnelScreens`, `createFunnelDraft`, `stageFunnelEdit`, `stageFunnelScreen`, `getFunnelDraft`, `listFunnelDrafts`, `discardFunnelDraft`, `saveFunnelEdits`.
|
|
8
8
|
|
|
@@ -18,8 +18,8 @@ The retention counterpart to campaigns — flows with no `campaignId`.
|
|
|
18
18
|
|
|
19
19
|
The pass (the wallet membership card) is now CLI-readable and -writable as a whole document.
|
|
20
20
|
|
|
21
|
-
- **`
|
|
22
|
-
- **`
|
|
21
|
+
- **`getPassConfiguration`** `{}` — returns the latest live configuration: `sections`, `features`, `locations`, `metadata`, and its `version`.
|
|
22
|
+
- **`updatePassConfiguration`** — **a full-document save, not a patch.** Anything you omit is dropped from the new version. The only safe workflow is read → modify the returned document → save the complete result. Saving appends a new version (history is preserved server-side), and a visible change triggers a re-push of the pass to every member's wallet — so this confirms before running and deserves the same care as a live send.
|
|
23
23
|
- Pass **image generation** (punch-card strips etc.) is not exposed — image workflows still need the app.
|
|
24
24
|
|
|
25
25
|
---
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@feastalytics/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
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": {
|