@feastalytics/cli 0.1.16 → 0.1.18

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.
Files changed (29) hide show
  1. package/README.md +12 -3
  2. package/dist/cli.js +170 -232
  3. package/package.json +6 -3
  4. package/skills/feast/SKILL.md +131 -0
  5. package/{feast → skills/feast}/references/domains.md +9 -7
  6. package/{feast → skills/feast}/references/links.md +11 -11
  7. package/skills/feast/references/setup.md +80 -0
  8. package/skills/feast/references/workflows/ad-copy-creator.md +92 -0
  9. package/skills/feast/references/workflows/ad-copy-guest.md +128 -0
  10. package/skills/feast/references/workflows/ads.md +62 -0
  11. package/skills/feast/references/workflows/automations.md +100 -0
  12. package/skills/feast/references/workflows/campaigns.md +75 -0
  13. package/skills/feast/references/workflows/creators.md +96 -0
  14. package/skills/feast/references/workflows/funnels.md +30 -0
  15. package/skills/feast/references/workflows/guests.md +38 -0
  16. package/skills/feast/references/workflows/members-program.md +30 -0
  17. package/skills/feast/references/workflows/onboarding.md +33 -0
  18. package/feast/SKILL.md +0 -105
  19. package/feast/references/setup.md +0 -70
  20. package/feast/references/workflows/ad-copy-creator.md +0 -94
  21. package/feast/references/workflows/ad-copy-guest.md +0 -130
  22. package/feast/references/workflows/ads.md +0 -50
  23. package/feast/references/workflows/automations.md +0 -98
  24. package/feast/references/workflows/campaigns.md +0 -43
  25. package/feast/references/workflows/creators.md +0 -90
  26. package/feast/references/workflows/funnels.md +0 -34
  27. package/feast/references/workflows/guests.md +0 -22
  28. package/feast/references/workflows/members-program.md +0 -26
  29. package/feast/references/workflows/onboarding.md +0 -34
package/feast/SKILL.md DELETED
@@ -1,105 +0,0 @@
1
- ---
2
- name: feast
3
- description: Operate a Feastalytics organization from the terminal — campaigns, automations, funnels, members-program rewards, the wallet pass, creator sourcing, Meta ads, onboarding, and read-only data queries — via the `feast` CLI. Use this skill whenever the user wants to inspect or change Feastalytics data outside the dashboard — "list my campaigns", "create an automation for org X", "approve this creator", "publish the recruitment ad", "query my guests", "update the members program", or any request to script/batch/automate Feastalytics operations. Reach for it even when the user doesn't say "CLI" — if the task is reading or changing Feastalytics data, this is the tool.
4
- ---
5
-
6
- # Feast CLI
7
-
8
- Drive the Feastalytics platform from the terminal. The `feast` CLI exposes the same tool surface the in-app AI agent uses (campaigns, automations, funnels, members program, creator sourcing, Meta ads, onboarding, data queries) as plain commands that hit the production API as the logged-in user.
9
-
10
- The CLI is the source of truth for *which* tools exist and *what* they accept — always discover that at runtime rather than assuming, because the tool set grows as new endpoints are tagged. Your job is to pick the right tool, scope it to the right organization, and hand it valid input.
11
-
12
- Some environments hand you the CLI already installed, already authenticated, and pinned to one organization. If `feast tools` runs, you're set — otherwise, installing, logging in, and staying current are in `references/setup.md`.
13
-
14
- ## The core loop: discover → describe → call
15
-
16
- Don't guess tool names or input shapes. Introspect the live CLI:
17
-
18
- ```bash
19
- feast tools # list every available tool, its domain, and whether it mutates
20
- feast describe <tool> # full description + input JSON schema for one tool
21
- feast call <tool> --org <organizationId> --input '<json>'
22
- ```
23
-
24
- Always `describe` an unfamiliar tool before calling it — the schema tells you the exact required fields, and the CLI validates your `--input` against it locally before sending anything, so a bad payload fails fast with a clear message instead of a confusing server error.
25
-
26
- That schema is also the boundary for what's worth asking the user about. Before sending a clarifying question, check whether the tool you're about to call has a field for the answer — a question about something the schema can't accept (a limit, a repeat rule, anything not in `describe`'s output) wastes a message and never gets used. Only ask about what the call in front of you can actually configure.
27
-
28
- ## Organizations: never let the API guess
29
-
30
- Most tools act on one organization, and which one must be explicit: pass it with `--org <organizationId>`. Acting on the wrong restaurant is worse than stopping to ask, so the CLI refuses rather than guessing when the target is ambiguous.
31
-
32
- If you weren't given an organization id, or a command reports you belong to several, `references/setup.md` has how to resolve one.
33
-
34
- ## Reads vs. writes
35
-
36
- Query tools (listing, describing, reading) are safe and read-only. Mutation tools (create, update, clone, delete, apply) change production data.
37
-
38
- - Mutations require `--org` explicitly.
39
- - 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.
40
- - **There is no confirmation prompt.** A mutation runs the moment you call it. Nothing asks twice, and nothing undoes it.
41
-
42
- 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. Paying a creator's bonus charges the organization's card. Publishing a campaign puts it live, and pricing a recurring promotion creates real Stripe products. Activating a Meta campaign spends real ad budget. 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. (The one schema-level exception: `publishAds` requires `confirm: true` in its input — but that's you confirming, not the CLI asking.)
43
-
44
- Prefer reading before writing: e.g. `listCampaigns` to find the right `campaignId` before `updateCampaign`, or `describe`/`listAutomationFlows` before creating a flow.
45
-
46
- ## Building good input
47
-
48
- `--input` takes a JSON string (or `--input-file <path>` for larger payloads). Construct it from the schema you got via `describe`. When a tool references another entity by id (a campaign id, location id, flow id), look that id up first with the relevant list/read tool rather than inventing it.
49
-
50
- For the domain-specific meaning of fields — how automations chain, what a funnel screen contains, how offers are structured — consult the guidance in `references/domains.md` when the schema alone isn't enough.
51
-
52
- ## Workflows
53
-
54
- Many tasks are multi-step and have a required ordering the app normally enforces. The most important rule: **automations live inside flows — always find a flow (`listAutomationFlows`) or create one (`createAutomationFlow`) before adding automations; never create an orphan automation.** The same "resolve the parent/ids first, then act" shape recurs across campaigns, funnels, and offers.
55
-
56
- **Before acting on any multi-step task, read the workflow file for it.** Each one carries the required call ordering and the domain rules that make the result good rather than merely valid — neither of which is in the tool schemas. Read it first; don't reconstruct the sequence from tool descriptions.
57
-
58
- | Doing this | Read |
59
- |---|---|
60
- | Creating, cloning or configuring a campaign; promotions | `references/workflows/campaigns.md` |
61
- | Anything touching automations — creating, editing, simulating, promoting a draft | `references/workflows/automations.md` |
62
- | Editing funnel screens, applying a funnel template, staging a new screen | `references/workflows/funnels.md` |
63
- | Writing guest-facing Meta ad copy (`adCopy`) | `references/workflows/ad-copy-guest.md` |
64
- | Writing creator-recruitment ad copy (`recruitmentAdCopy`) | `references/workflows/ad-copy-creator.md` |
65
- | Publishing, pausing, budgeting or diagnosing Meta ads | `references/workflows/ads.md` |
66
- | Creator sourcing — approving applicants, reviewing content, creatives, payouts | `references/workflows/creators.md` |
67
- | Members-program rewards; reading or saving the wallet pass configuration | `references/workflows/members-program.md` |
68
- | Working the onboarding taskboard; brand identity; phone, media, invites, billing | `references/workflows/onboarding.md` |
69
- | Searching guests/members; querying anything via the data catalog | `references/workflows/guests.md` |
70
-
71
- 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.
72
-
73
- 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.
74
-
75
- 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.
76
-
77
- 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.
78
-
79
- ## Link to what you touched
80
-
81
- Work you do through the CLI lands somewhere in the product, and a link is a cheap thing to offer — so offer them freely. After a turn where you created, changed, or published something, close with a short markdown list: where to see it, where to edit it, where to preview it. Not because anyone has to go check your work, but because opening the thing is usually the next step anyway. When someone asks where a thing lives or how to set it up, lead with the link rather than click-by-click directions.
82
-
83
- **You don't know these URLs — read `references/links.md` before you write one.** The dashboard's shape is not the one you'd extrapolate from the guest-facing links elsewhere in this skill, so a URL that looks obviously right is the exact case to check. A wrong link is worse than no link: it looks authoritative and 404s.
84
-
85
- That file has the dashboard routes with their panel and tab names, the guest-facing pages on the organization's own subdomain, the preview route that completes the funnel draft loop, and which query params actually suppress analytics versus merely tagging a visit as a preview.
86
-
87
- ## Worked example
88
-
89
- User: "add a Free Dessert reward members can redeem for 100 points in my Plum Vietnamese org."
90
-
91
- ```bash
92
- feast whoami # only if you weren't given the org id already
93
- feast describe createMembersProgramReward # learn the input shape (type: item vs name)
94
- feast call listMembersProgramRewards --org <orgId> # avoid duplicating an existing reward or catalog item
95
- feast call createMembersProgramReward --org <orgId> --input '{"type":"name","name":"Free Dessert","pointsCost":100}'
96
- ```
97
-
98
- The pattern generalizes: identify the org, learn the tool, resolve any referenced ids, then act.
99
-
100
- ## When something fails
101
-
102
- - "Not logged in / session expired", or `feast` isn't on PATH → `references/setup.md`.
103
- - "You belong to multiple organizations" → pick one with `--org`; `references/setup.md` has how to find the id.
104
- - "Input does not match the tool schema" → re-read `feast describe <tool>` and fix the named fields.
105
- - A tool you expected isn't listed by `feast tools` → it may not be exposed yet; don't fabricate a call, tell the user.
@@ -1,70 +0,0 @@
1
- # Setup, auth, and organizations
2
-
3
- One-time and troubleshooting material: getting the `feast` CLI installed and logged in, keeping it and this skill current, and working out which organization to act on. The day-to-day loop lives in `SKILL.md`; you only need this file when something isn't working yet.
4
-
5
- Some environments hand you a CLI that is already installed and authenticated, and pin you to a single organization. Nothing in this file applies there — if `feast tools` runs and your commands are going to the right restaurant, you are already set up.
6
-
7
- ## Installing
8
-
9
- The `feast` CLI must be installed and on PATH:
10
-
11
- ```bash
12
- npm install -g @feastalytics/cli # or run ad-hoc with: npx @feastalytics/cli <command>
13
- ```
14
-
15
- If the global install fails on permissions, don't retry with `sudo` — tell the user and fall back to `npx @feastalytics/cli@latest`.
16
-
17
- ## Authenticating
18
-
19
- Authenticate once — tokens are cached in `~/.config/feast-cli/credentials.json` and refreshed automatically:
20
-
21
- ```bash
22
- feast login # opens a browser to authorize (default)
23
- feast login --password [username] # headless / CI: username + password prompt, no browser
24
- ```
25
-
26
- If a command reports you're not logged in or the session expired, re-run `feast login`.
27
-
28
- ## Staying current
29
-
30
- Neither the CLI nor this skill updates itself. When a command prints an update notice on stderr:
31
-
32
- ```
33
- Update available: feast 0.1.1 → 0.2.0
34
- ```
35
-
36
- update both, then tell the user in one line that you did:
37
-
38
- ```bash
39
- npm install -g @feastalytics/cli@latest # only if `feast` is already on PATH from a global install
40
- npx skills add feastalytics/cli -g -a '*' -y # refresh this skill from the repo
41
- ```
42
-
43
- If you've been invoking the CLI through `npx` rather than a global install, skip the `npm install -g` and use `npx @feastalytics/cli@latest <command>` for the rest of the session instead — `npx` reuses a cached copy otherwise.
44
-
45
- Update the skill whenever you update the CLI: the two ship from the same repo but on different triggers, so a new CLI version usually means this skill's guidance has moved too.
46
-
47
- ## Playbook skills
48
-
49
- Feastalytics publishes further skills that build on this one (campaign diagnosis and other playbooks). They come from the API, not from GitHub:
50
-
51
- ```bash
52
- feast skill list # what is published for your organization
53
- feast skill install feast-playbooks # install or refresh one into ~/.claude/skills/
54
- ```
55
-
56
- Install what `feast skill list` offers when the user asks for a playbook this skill does not cover, and re-run the install when the CLI prints an update notice.
57
-
58
- ## Which organization
59
-
60
- Most tools act on one organization. A user often belongs to several, so which one you target matters and must be explicit.
61
-
62
- ```bash
63
- feast whoami # shows the logged-in user and every org (with names) they can act on
64
- ```
65
-
66
- Pass the target org with `--org <organizationId>`:
67
-
68
- - If the user names an org, resolve it to its id with `feast whoami` and pass that id.
69
- - If the user belongs to exactly one org, the CLI uses it automatically — no flag needed.
70
- - If they belong to more than one and you omit `--org`, the CLI refuses and lists the orgs rather than silently picking one. That's intentional: acting on the wrong org is worse than stopping to ask. When this happens, surface the list to the user and confirm which one they mean.
@@ -1,94 +0,0 @@
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,130 +0,0 @@
1
- # Guest-facing ad copy (`adCopy`)
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 guest-facing half of Meta ad copy: `adCopy`, which sells the offer and the food to a hungry local scrolling past.
6
-
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.
8
-
9
- Publishing the finished ad is a separate job with its own loop — read `ads.md` for that.
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 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.
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
- ## Read before you write
22
-
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:
24
-
25
- 1. `getOrganization` — brand name, cuisine, and the subdomains in `subdomains2[].subdomain`.
26
- 2. `getCampaign` — `name`, `description`, `bannerConfig`, `promotions`, `referrers`, `shorthand`.
27
- 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.
28
- 4. `queryData` on `interface.catalogItem` — real menu item names and real prices, not approximations of them. It's hierarchical; walk the tree with `parentId` or `catalogItemLink`.
29
-
30
- 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.
31
-
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.
33
-
34
- ## Headlines — each ≤ 40 characters
35
-
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.
37
-
38
- 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:
39
-
40
- 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.
41
- 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.
42
- 3. **Social proof** — popularity or local reputation ("The neighborhood's worst-kept secret"). Borrows credibility the restaurant already earned.
43
- 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.
44
- 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.
45
-
46
- No generic marketing language. Write like a person, not a brand.
47
-
48
- ## Primary text — each 2–4 sentences
49
-
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.
51
-
52
- **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.
53
-
54
- - No hashtags.
55
- - No "click the link below" / "tap below" — Meta owns the CTA button, and pointing at a link that isn't there is just confusing.
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.
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).
58
-
59
- ## Voice
60
-
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.
62
-
63
- - Short punchy fragments > grammatically perfect sentences.
64
- - Confidence and excitement > polite and formal.
65
- - Specific details > vague claims ("crispy baguette with savory fillings" > "delicious food").
66
- - Talk **to** the reader, not **at** them.
67
-
68
- **BAD** (robotic, corporate, flat):
69
-
70
- ```
71
- 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.
72
- ```
73
-
74
- **GOOD** (energetic, specific, scroll-stopping):
75
-
76
- ```
77
- Free Coconut Matcha with any banh mi. 🍵
78
-
79
- Yeah, you read that right.
80
-
81
- Crispy baguette, savory fillings, and a specialty coffee on the house. Grab your voucher before this one's gone.
82
- ```
83
-
84
- **BAD:**
85
-
86
- ```
87
- 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.
88
- ```
89
-
90
- **GOOD:**
91
-
92
- ```
93
- Crispy baguette + savory fillings + a free specialty coffee? 👏
94
-
95
- That's lunch sorted.
96
-
97
- Claim your voucher and come see what the hype is about.
98
- ```
99
-
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.
101
-
102
- ## `creativeMix` changes what the copy may assume
103
-
104
- Set this to what's actually true of the assets that will run, then write to it:
105
-
106
- - **`static_only`** — the offer is printed on the image. Reference it directly; the viewer always sees it.
107
- - **`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.
108
- - **`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").
109
-
110
- Copy that only makes sense next to a visible offer, running as `mixed`, will quietly underperform for half the audience.
111
-
112
- ## Video-led campaigns: the one thing you can't do
113
-
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.**
115
-
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.
117
-
118
- ## Saving it
119
-
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.
121
-
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.
123
-
124
- ## Publishing
125
-
126
- **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.
127
-
128
- ---
129
-
130
- > **Not exposed:** ad copy generation (write it yourself, per above). Read `references/links.md` before writing any dashboard link you hand over.
@@ -1,50 +0,0 @@
1
- # Publishing and steering 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
- 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
-
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
-
9
- ### The model: template → plan → publish → activate
10
-
11
- - A **template** is a server-owned recipe. `listAdTemplates` returns each one with its variables, budget range, and which plan paths may be overridden. Each variable that names a `producedBy` tool is telling you exactly where its value comes from — treat that as the shopping list.
12
- - **`planAds`** resolves template + variables into the exact tree of campaigns, ad sets and ads that would be created. It creates nothing on Meta and changes no Feastalytics data. It returns the tree, a `planHash`, the fully defaulted variables, and validation issues.
13
- - **`publishAds`** takes those variables, overrides and hash back *unchanged*, re-derives the tree server-side, and refuses on a mismatch — so a stale plan fails loudly instead of publishing something the human never saw. Everything is created **paused**.
14
- - **`setAdCampaignStatus`** `ACTIVE` starts a campaign Feastalytics published, cascading to every ad set and ad. This is the moment real money starts moving — explicit user confirmation first, every time.
15
-
16
- ### The loop
17
-
18
- 1. `listAdTemplates` — pick the template, read each variable's `producedBy`.
19
- 2. Gather variables with those tools: `ads_get_ad_accounts`, `ads_get_user_pages`, `ads_get_ig_accounts`, `listCreatives`, `getCampaign`, etc. Prefer a Page with `usedByOrganization: true` — the token reaches other businesses' Pages and nothing stops you publishing from the wrong one.
20
- 3. `planAds` — fix every issue with severity `error` and re-plan. Summarize the resulting tree (campaign name, budget, targeting, ad count) for the user before going further; the plan is the thing they're approving.
21
- 4. `publishAds` with the returned `variables`, `overrides` and `planHash` unchanged, plus:
22
- - `confirm: true` — the schema demands it; this is the only tool with a schema-level confirm.
23
- - an `idempotencyKey` you generate. Reuse the same key when retrying the *same* publish — a duplicate key returns the earlier job instead of publishing twice. Never reuse one for a new publish.
24
- - `effects` — see below.
25
- 5. Poll `getJob` with the returned `jobId` + `jobType` until `COMPLETED` or `FAILED`. `{ job: null }` means not landed yet — keep polling. **Read the job's effect outcomes** — each declared effect reports `done`, `skipped` or `error` with a human-readable detail, and effect failures do not fail the job (the ads already exist by then), so this is the only place you find out.
26
- 6. `setAdCampaignStatus` to go live, after the user says go. Check the preflight counts in the response.
27
-
28
- ### Effects: the write-back is declared, not called afterwards
29
-
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
-
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
- - **`linkFeastCampaign`** records the published Meta campaign onto a Feast campaign, which is what makes its ads panel and KPIs see the spend.
34
-
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
-
37
- ### Which template
38
-
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
- - **`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
-
43
- ### Reading and steering what's live
44
-
45
- - `ads_get_ad_entities` — read campaigns/ad sets/ads on an account, creatives attached. The diagnostic read for everything below.
46
- - `ads_update_entity` — rename, re-budget, or pause. Budgets are integer cents and **replace** the current value; read first, confirm the number with the human. Creatives are immutable at Meta — new copy or media means a new ad (the `addAds` template).
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
- - `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
-
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.
@@ -1,98 +0,0 @@
1
- # Automations
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
- **Fully authorable** (create / edit / delete / dry-run). This is the richest workflow — the ordering is simpler than the app's, but the domain rules below are what separate a professional flow from a carrier-blocked mess. Follow them when creating, and use them as a checklist when reviewing.
6
-
7
- ### The model: automations live inside flows
8
-
9
- - An **automation** is one trigger → (conditions) → action unit (e.g. "on checkout, send a text").
10
- - A **flow** groups automations by a shared trigger, and belongs to *either* a campaign *or* the members program — never both.
11
- - **The rule that matters most: every automation needs a `flowId`. `batchEditAutomations` throws on a create op without one.** So you must resolve the flow *before* creating. Never invent a flowId.
12
-
13
- ### The CLI loop: draft → stage → share → save
14
-
15
- Automations have a staging tier, and it is the default path. Changes accumulate on a **draft** — an off-prod overlay — until someone explicitly promotes them. A draft carries a link that renders the change as a text-message thread with the edits highlighted, which is what you hand a human to look at before anything reaches a real guest.
16
-
17
- 1. `listAutomationFlows` — find an existing flow. Pass `{ "campaignId": "<id>" }` for a campaign's flows, or `{ "scope": "membersProgram" }` for members-program flows. Reuse a matching flow when one fits.
18
- 2. If none fits, `createAutomationFlow` to make one. If the campaign/members-program has **no flows at all**, strongly prefer `applyAutomationTemplate` (then customize) over building from scratch. Only apply a template when there are no existing flows.
19
- 3. `listAutomations` with `{ "flowId": "<id>" }` to see the automations already in that flow before editing (omit the input to list every automation in the org). **Do this immediately before every `update` op you stage, not just once at the start of the session.** An `update` replaces an automation's entire `actions` array — it does not merge one action in. If you reconstruct `actions` from an earlier tool result or from what you remember discussing, rather than the automation's current live state, you silently drop whatever isn't in your reconstruction (a reward grant, a task action, anything not under discussion in that turn). This is true even a few messages later in the same conversation, once the user has asked for a second or third change to the same automation.
20
- 4. `createAutomationDraft` with a short `title` describing the change in the user's terms ("Shorten the day-3 nudge") — that title is what the reviewer sees. Keep the returned `draftId`; **there is no way to list drafts, so if you lose it the draft is unreachable.**
21
- 5. `stageAutomationEdits` with `{ "draftId": "<id>", "operations": [...] }`. The ops are exactly the ones `batchEditAutomations` takes:
22
- - `{ "type": "create", "automation": { ...full automation..., "flowId": "<id>" } }` — generate a fresh UUID for the automation's id, set the `flowId`, and include triggers, conditions, actions, send time, and a descriptive title all at once. Create ops require the flowId.
23
- - `{ "type": "update", "automationId": "<id>", "automation": { ...changed fields... } }`
24
- - `{ "type": "delete", "automationId": "<id>" }` — blocked at save time if the automation already has sends.
25
- Call it repeatedly to build a change up; ops append in order.
26
- 6. `simulateAutomations` with `{ "flowId": "<id>", "edits": <the draft's operations> }` — dry-run against a synthetic event timeline with **no real sends** and confirm the right automations fire. If the simulation surprises you, stage a correction rather than promoting and patching live.
27
- 7. **Give the user the `previewUrls` from the draft** and let them look before you promote. Each entry is one flow's before/after view. Don't promote unprompted work on the user's behalf — staging exists so a human sees the change first.
28
- 8. `saveAutomationEdits` with `{ "draftId": "<id>" }` — this is the write to production. It refuses if any automation the draft touches was changed by someone else since you staged, naming which; re-stage against the current state rather than retrying.
29
-
30
- `discardAutomationDraft` throws a draft away without promoting. `getAutomationDraft` re-reads one by id. Drafts expire after 14 days.
31
-
32
- **`batchEditAutomations` still exists and writes straight to production in one call.** Reach for it only when the user explicitly wants an immediate live change and has said so — not as a shortcut past the review step.
33
-
34
- `updateAutomationFlow` renames/retitles a flow; `deleteAutomationFlow` removes a flow and its automations (blocked at ≥20 sends — turn it off instead).
35
-
36
- > **Not exposed:** actually *firing* an automation at a live member (the app's "run") is intentionally not a CLI tool — it sends a real SMS. Use `simulateAutomations` for verification; real sends stay in the app.
37
-
38
- ### Choosing the trigger
39
-
40
- - **Campaign flows: prefer `viewCampaign` over `signUp`.** A guest viewing the campaign page is the natural entry point — it captures new sign-ups *and* returning guests. Use `signUp` only for members-program welcome flows or a fire-once-at-registration moment.
41
- - **`offerExpiration` is rarely a *flow* trigger.** Use it on an individual automation inside an expiration nurture chain, not as a standalone flow's trigger type.
42
-
43
- ### Conditions: the nested event/occur shape
44
-
45
- Event conditions nest the event and its timing. The `occur` object uses `match` (GTE/LTE/EQ) and `duration` (milliseconds):
46
-
47
- ```json
48
- { "type": "event",
49
- "event": { "event": { "type": "signUp" },
50
- "occur": { "match": "GTE", "duration": 86400000 } } }
51
- ```
52
-
53
- - **Positive duration = past** (event already happened) — for `signUp`, `addPass`, `visit`, `offerRedemption`, etc.
54
- - **Negative duration = future** — only for `offerExpiration` (e.g. "expires within 2 hours" → `LTE`, `-7200000`).
55
- - `EQ` matches within the whole increment (day/week/hour).
56
-
57
- ### Send times & prime texting windows
58
-
59
- Send times are `immediate`, `relativeDelay` (`delayMs` after the trigger), or `absoluteDelay` (`utcHour`/`utcMinute`, optional `utcDayOfWeek` or `utcDay`). Think in the **org's local timezone**, then convert to UTC.
60
-
61
- **Always schedule inside a prime window — never arbitrary times, never before 8 AM or after 9 PM:**
62
- - Morning: **8:00–11:30 AM** (org timezone)
63
- - Afternoon: **4:00–6:00 PM** (org timezone)
64
-
65
- Which window depends on meal service: breakfast/lunch-only → all morning; dinner-only → mostly afternoon, at most one morning; both → roughly 50/50. Determine meal service from existing automations/funnel/settings, or ask the user. **Vary the minutes** so no two automations in a flow share a send time (e.g. 9:03, 9:17, 4:22). Recommend specific times rather than asking.
66
-
67
- ### Chaining vs. keeping independent
68
-
69
- Chain with the `receiveAutomation` trigger (automation B fires because A was received) **only when B always follows A**.
70
-
71
- - **Good:** welcome → follow-up tips 2 days later; expiration nurture reminders (per-guest timeline).
72
- - **Do NOT linearly chain a calendar countdown** ("1 week before" → "3 days before" → "day of"). If an early step fails or the guest joins late, every later step is blocked. Instead **fan out from a shared parent**: every countdown message uses `receiveAutomation` → the same entry automation, each with its own `absoluteDelay` date. Then no single message can block the rest.
73
- - **Expiration loops are valid** when gated by user action + a state change: e.g. `... → expired → (guest texts EXTEND, a reply-trigger automation runs extendReward) → re-enters "expires in 3 days"`. The loop is safe because EXTEND gates re-entry and `extendReward` moves the expiration date so conditions re-evaluate. A loop with no user action or no state change is invalid (infinite).
74
-
75
- ### Backfill (chained automations against past recipients)
76
-
77
- When an automation's trigger is `receiveAutomation`, ask the user whether it should apply only going forward or also to everyone who already received the upstream automation:
78
-
79
- - Default `applyToHistorical: false` (going forward only).
80
- - For past recipients, set `applyToHistorical: true` as a **top-level sibling** of `automation` on the create/update op (never inside a trigger; it isn't stored — it only enqueues a one-shot backfill on that save).
81
- - Before confirming, call `countParentAutomationRecipients` and tell the user the audience size; warn if > 1000. Only backfill after explicit confirmation.
82
-
83
- ### Rewards inside automations
84
-
85
- - **Checkout auto-creates the reward.** For a checkout-triggered automation, do NOT add an `awardReward` action — the reward is already granted. Checkout flows only send texts. Use `awardReward` for non-checkout flows (visit milestones, sign-up rewards).
86
- - **Members-program `awardReward` defaults to a 30-day expiration**: `{ "type": "relative", "relative": { "offsetMs": 2592000000 } }`. Mention it in your summary; omit only if the user says the reward shouldn't expire. This does not apply to campaign offers.
87
-
88
- ### Text-content best practices (rules when creating, checklist when reviewing)
89
-
90
- 1. **Descriptive names** — "Day 2 – Visit Reminder with Pass Link", not "Reminder 1".
91
- 2. **Lead with the pass link** — the first post-signup text MUST include it ("add your pass: {{pass link}}").
92
- 3. **Always `https://`** on every link (carriers block bare/protocol-less links).
93
- 4. **Mobile Google Maps links only** — `https://maps.app.goo.gl/...`, never desktop `maps.google.com`.
94
- 5. **Correct reservation links** — `https://{subdomain}.feastalytics.com/i/{shorthand}/reservation` using the *current* campaign's shorthand (from `listCampaigns`) and a valid subdomain. Never reuse another campaign's link.
95
- 6. **Personalize** with `{{firstName}}`; **vary** tone/wording across automations; **re-share** useful info (pass link, hours, maps, reservation) in reminders; keep **empty lines** between blocks for readability.
96
- 7. **Align offer expirations with open hours** — never expire an offer while the restaurant is closed.
97
-
98
- ---
@@ -1,43 +0,0 @@
1
- # Campaigns and offers
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
-
6
- ## Creating a campaign
7
-
8
- Fully doable via the CLI. The server does the heavy lifting (id generation, default config, the funnel prerequisite) — you sequence the calls.
9
-
10
- 1. `getOrganization` — read the org to get valid **referrers** (subdomains, from `subdomains2[].subdomain`) and location ids.
11
- 2. `createCampaign` with `{ "campaign": { "name": "...", "isCreating": true, "fbCampaigns": [], "attributionRules": [] } }` — keep the returned campaign **id** (a UUID). It comes back `isCreating: true`.
12
- 3. `populateCampaign` with the `campaignId` and a **`funnelType`**:
13
- - `"reservation"` — no extra config.
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
- - `"prepay"` — needs `prepayConfig` with `promotionName`, `price`, and an image (`imageUrl`).
16
- 4. (optional) the funnel — the **acquisition** half: the funnel screens a guest sees. Same list → pick → apply shape as automations:
17
- - `listFunnelTemplates` with the `campaignId` — the template catalog with per-campaign `eligible`/`ineligibleReason`, a `recommended` id, and each template's guest `journey`. Read this before applying; never guess a template id.
18
- - Pick by what the guest should experience, not by whether the offer has a price:
19
- - `offer-basic` — Sign Up goes straight to the offer wallet. **No payment step.** The template for any offer redeemed in person, priced or not.
20
- - `offer-prepay` / `offer-direct-prepay` — a Stripe payment screen is part of the funnel. Only eligible when the promotion has `canPrePay: true` **and** a `price`; anything else is rejected with `PRECONDITION_FAILED`.
21
- - `reservation-offer-basic` / `reservation-offer-prepay` / `reservation-offer-direct-prepay` / `reservation-only` — the reservation variants of the same split.
22
- - `applyFunnelTemplate` with `{ "campaignId": ..., "templateId": ... }`. Requires a fresh campaign whose funnel is unset; resolves the referrer from the campaign.
23
- - The promotion's `canPrePay` flag does **not** change what a template builds — it only gates eligibility. A "no prepay" request means `offer-basic` (or another no-payment template), full stop.
24
- - After applying, confirm with `listFunnelScreens` that the journey matches intent — for a no-prepay offer there must be no `payment` screen.
25
- 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`.
26
-
27
- 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.
28
-
29
- **Reading a campaign back:** `getCampaign` returns the full config for one campaign (funnel/offer config, referrers, status); `listCampaigns` is the summary list; `getCampaignKpis` is performance metrics. Read with `getCampaign` before any `updateCampaign`.
30
-
31
- **Cloning:** `cloneCampaign` with `sourceCampaignId`, `newCampaignName`, and a `referrer` (subdomain) duplicates funnel + automations + offers and returns a `newCampaignId`. **Gotcha:** the cloned automations still contain the *source* campaign's reservation links. After cloning, review the new campaign's automations and rewrite any reservation link to the new campaign's shorthand — the format is `https://{subdomain}.feastalytics.com/i/{new-shorthand}/reservation`.
32
-
33
- The one-shot text→campaign endpoints (`createWithOffer` / `parseCampaignDescription`) aren't exposed to the CLI — use the steps above.
34
-
35
- ---
36
-
37
- ## Offers and promotions
38
-
39
- - A campaign's **promotions** are part of the campaign record: read them with `getCampaign`, edit them with `updateCampaign` (including a promotion's `staffInstructions`, and prices — noting the Stripe-products warning in `updateCampaign`'s description).
40
- - **Real menu data** for grounding any offer or promotion copy comes from `queryData` on `interface.catalogItem` — POS-agnostic, hierarchical via `parentId`/`catalogItemLink`.
41
- - When you write guest-facing offer language anywhere, frame it as an "offer," never a "discount" or "deal."
42
-
43
- ---