@feastalytics/cli 0.1.5 → 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/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, so the CLI adds guards:
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 verifies the server-resolved org and prompts for `y/N` confirmation.
80
- - In a non-interactive context where the user has already told you to proceed, add `--yes` to skip the prompt. Only do this when the user's intent is unambiguous — the confirmation exists to prevent acting on the wrong org or with the wrong payload.
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
 
@@ -91,7 +93,22 @@ For the domain-specific meaning of fields — how automations chain, what a funn
91
93
 
92
94
  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.
93
95
 
94
- For the ordered steps and domain rules of each common workflow, read `references/workflows.md`. It covers what's **fully doable** — creating/cloning a campaign, authoring automations end-to-end (draft-first: stage edits on a draft, dry-run them with `simulateAutomations`, hand the user the preview link, then promote), editing funnel screens (the draft → preview → promote loop, including staging brand-new screens), reading and saving the wallet pass configuration (full-document save — read, modify, save the whole thing), listing/creating members-program rewards, creating offers, and exploring users — and what's **not yet exposed** — reward update/delete, pass image generation, brand identity, and replying to guests by SMS. 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.
96
+ **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.
97
+
98
+ | Doing this | Read |
99
+ |---|---|
100
+ | Creating, cloning or configuring a campaign; offers in the strategy backlog | `references/workflows/campaigns.md` |
101
+ | Anything touching automations — creating, editing, simulating, promoting a draft | `references/workflows/automations.md` |
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` |
104
+ | Creator sourcing — approving applicants, reviewing their content, conversations | `references/workflows/creators.md` |
105
+ | Members-program rewards; reading or saving the wallet pass configuration | `references/workflows/members-program.md` |
106
+ | Working the onboarding taskboard; brand identity | `references/workflows/onboarding.md` |
107
+ | Searching guests/members and their activity | `references/workflows/guests.md` |
108
+
109
+ Read more than one when a task spans them — a new campaign usually means `campaigns.md` plus `automations.md` and `funnels.md`.
110
+
111
+ Some things are deliberately **not exposed**: replying to a guest or a creator by SMS, firing an automation at a live member, reward update/delete, pass image generation, and most of the creator pipeline beyond the two approval decisions. 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.
95
112
 
96
113
  ## Link to what you touched
97
114
 
@@ -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 `loadCurrentOrganization` (it returns the org's `locations`) — not the organization id.
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
 
@@ -20,8 +20,8 @@ Typical flow: `createCampaign` (set `isCreating: true` if you'll finish it with
20
20
  - An **automation** is one trigger → action unit (e.g. "on checkout, award reward").
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
- - **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.md` for the ordering and the trigger/condition/send-time rules.
24
- - Templates: `listAutomationTemplates` → `loadTemplateAutomations` (preview) → `applyAutomationTemplate`. Only apply a template to a campaign/members-program that has no existing flows.
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` → `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
 
@@ -33,6 +33,10 @@ Settings tabs: `account`, `general`, `members`, `integrations`, `notifications`,
33
33
 
34
34
  Funnels are always edited inside a panel, never on a page of their own — a campaign's `funnel-v2` panel, or the members program's `funnel` panel.
35
35
 
36
+ ### Onboarding task pages
37
+
38
+ Task entries from `getTaskboard` come with a ready-made `completionUrl` — always prefer pasting that over constructing a URL. The shape behind it: `https://feastalytics.com/tasks/<organizationId>` is the org's standalone task list, and `https://feastalytics.com/tasks/<organizationId>/<taskId>` opens one task's completion UI directly (chrome-less; works in the dashboard's agent preview panel and as a normal browser link). These are the links to hand over when a task needs the human — OAuth connections, phone purchase, device setup.
39
+
36
40
  ### Automation previews
37
41
 
38
42
  These two hang off the **root**, not off `/<organizationId>/app` — the organization id is the first path segment:
@@ -57,9 +61,9 @@ The guest-facing site lives on the organization's own subdomain, `https://<subdo
57
61
  /preview/<draftId>/<campaignId> the same draft, scoped to one campaign
58
62
  ```
59
63
 
60
- Get `<subdomain>` from `loadCurrentOrganization` → `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.
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.
61
65
 
62
- The `/preview/<draftId>` route is the payoff of the draft → preview → promote loop in `workflows.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.
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.
63
67
 
64
68
  ## Two query params that don't do what they look like
65
69
 
@@ -0,0 +1,98 @@
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
+ **Now fully authorable from the CLI** (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).
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
+ ---
@@ -0,0 +1,46 @@
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) `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
+
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
+
21
+ **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`.
22
+
23
+ **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`.
24
+
25
+ The one-shot text→campaign endpoints (`createWithOffer` / `parseCampaignDescription`) aren't exposed to the CLI — use the steps above.
26
+
27
+ ---
28
+
29
+ ## Creating offers (strategy backlog)
30
+
31
+ Fully doable from the CLI. Offers live in the organization's strategy backlog (the offer queue), sourced from real menu data.
32
+
33
+ 1. `getOrganization` → get the **`locationId`** (from `locations`) — offer tools key on the location, **never** the organizationId.
34
+ 2. `dfyGetMenuHierarchy` with that `locationId` — browse real menu items and prices.
35
+ 3. `dfyListOffers` — see the current backlog; avoid duplicates.
36
+ 4. `dfyCreateOffer` — create it. `dfyUpdateOffer` / `dfyDeleteOffer` to revise.
37
+
38
+ Every offer picks one **framework**:
39
+
40
+ - **`free`** — give away a low-cost item (appetizer, side, drink, small dessert) with no purchase. Maximizes signups. `offerPrice: null`; `items` = the single free item at its menu price. Headline: "A Complimentary [Item]" / "Free [Item]".
41
+ - **`combo`** — bundle to lift the ticket. *Pattern A* "Buy X, Get Y Free" (`offerPrice` = purchased item only) — best for quick-service. *Pattern B* fixed-price bundle "[Item] & [Item] for $XX" (`offerPrice` = bundle price) — preserves brand equity for upscale.
42
+ - **`experience`** — a curated multi-item tasting/pairing/prix-fixe with **no discount** (`offerPrice` = sum of item prices). MUST have 2+ items.
43
+
44
+ `dfyCreateOffer` needs `name` (internal label, no restaurant name), `headline` (states what the guest gets + price, using real item names), a short `description` (context the headline can't carry), `framework`, `items` (`[{name, price}]` from the menu), `offerPrice`, and `locationId`. Always frame as "offers," never "discounts" or "deals."
45
+
46
+ ---
@@ -0,0 +1,73 @@
1
+ # Creator sourcing
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
+ Restaurants recruit local creators to visit and post about them. A recruitment ad brings applicants in; from there it's a queue of decisions — approve the applicant, then later approve the content they made. Both decisions text the creator, so neither is a quiet status change.
6
+
7
+ ### The model: the application IS the visit row
8
+
9
+ There is no separate application object. One row covers a creator's whole journey with a location, and you read the stage off its columns rather than a single status field:
10
+
11
+ - `approvalStatus` `pending_approval` → awaiting your decision, then `approved` or `denied`.
12
+ - `startTime` **null** on an approved row → they're approved but haven't booked yet. Set → scheduled.
13
+ - `preVisitConfirmationStatus` `confirmed` → they confirmed they're still coming.
14
+ - `postVisitFollowUpSentAt` set → the visit is done.
15
+
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
+
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
+
53
+ ### The decision loop
54
+
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.
56
+ 2. `approveCreatorVisit` with `{ "eventId": "...", "decision": "approved" | "denied" }`. **This texts the creator immediately** — approved sends their booking link and creative brief, denied sends a decline. It also **consumes the location's monthly creator sourcing allowance**, and recruitment auto-pauses once that limit is reached, so an approval is both a message and a spend. Confirm with the user before working through a queue; don't batch-approve on your own initiative.
57
+ 3. The creator books, visits, and submits content on their own — none of that is driven from here.
58
+ 4. `listCreatorSubmissions` with `{ "status": "submitted" }` (and `"revision_requested"`) — the content review queue. Submissions are stored outside the queryable data model, so this tool is the only way to read them.
59
+ 5. `decideCreatorSubmission` — `approved`, `rejected`, `revision_requested`, or `under_review`. **This texts the creator too.** `revision_requested` sends your `feedbackMessage` verbatim plus a resubmit link, so write it as something the creator will read, not an internal note. Approving queues their bonus payout. `approvalType` defaults to `"ad"` (the content may run in paid ads) and is rejected when the board's bonus is $0 — use `"organic"` when it's just for their own channels.
60
+
61
+ ### Conversations
62
+
63
+ `listCreatorConversations` is the "who is waiting on a reply" queue: every creator's SMS thread with `hasUnread`, the last message body and direction, and a derived `visitStatus` chip that's more reliable than reading raw columns.
64
+
65
+ **You cannot reply from the CLI, and you cannot clear the unread flag.** Both stay in the dashboard — texting a creator back is the highest-consequence action in this area. Surface who's waiting and what they said, then hand the user the conversation.
66
+
67
+ ### Everything else: queryData
68
+
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.
70
+
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.
72
+
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
+ ---
@@ -0,0 +1,29 @@
1
+ # Funnels
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
+ **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
+
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
+
9
+ ### The loop
10
+
11
+ 1. **`listFunnelScreens`** `{ "referrer": "<subdomain>", "campaignId": "<id>" }` — read the funnel's screens to get each `screenId` and its renderables' `id`s + content. **Read before any `update` edit** — an update replaces a renderable by id, so you need its current shape. (Omit `campaignId` for a base/members-program funnel.)
12
+ 2. **`createFunnelDraft`** `{ "referrer": "...", "campaignId": "..." }` — creates an off-prod overlay; keep the returned `draftId`. Nothing is live yet. **Immediately inspect the funnel's current state**: open the draft's preview URL (see step 4 — with no edits staged yet it renders the live funnel as-is) so you have a visual baseline of what you're about to change. If you have a browser/screenshot tool, open and screenshot it now; if you can't view it yourself, share the URL with the user before editing.
13
+ 3. **`stageFunnelEdit`** `{ "draftId": "...", "screenId": "...", "edit": <RenderableEdit> }` — one renderable edit per call; the server validates it against the current screen. Repeat per change. A `RenderableEdit` is a discriminated union (`describe stageFunnelEdit` for the full schema):
14
+ - `{ "type": "update", "id": "<renderableId>", "renderable": { ...clone of what you read, with your changes... } }` — keep the same `id`.
15
+ - `{ "type": "create", "renderable": { "id": "<new-uuid>", ... }, "targetId": "<sibling id>", "position": "before" | "after" | "inside" }` — generate a fresh UUID.
16
+ - `{ "type": "delete", "id": "<renderableId>" }` and `{ "type": "move", "id": "...", "targetId": "...", "position": "..." }`.
17
+ **Need a brand-new screen?** `stageFunnelScreen` `{ "draftId": "...", "title": "...", "description"? }` stages an empty screen on the draft and returns its **permanent `screenId`** — the id is assigned at staging time, not at save, so you can immediately `stageFunnelEdit` content into it and reference it from navigation/buttons on other screens; nothing gets re-keyed at promote. On a campaign draft the screen is campaign-scoped unless you pass `"campaignScoped": false`. The screen only exists on the draft (and in draft-scoped `listFunnelScreens`/previews) until `saveFunnelEdits` promotes it, which reports it in `createdScreenIds`.
18
+ 4. **Preview** (no CLI call): open `https://{referrer}.feastalytics.com/preview/{draftId}/{campaignId}` (drop `/{campaignId}` for a members-program draft). It renders the whole funnel as a flow diagram with the draft's edits applied. Screenshot it, then iterate — re-run `listFunnelScreens` **with the `draftId`** to read the funnel *with* the staged edits, stage more, re-preview — until it's right.
19
+ 5. **`saveFunnelEdits`** `{ "draftId": "..." }` — **promotes to prod** (mutation, confirms first): applies the draft's edits to the live funnel and marks the draft `promoted`. To abandon instead, `discardFunnelDraft`.
20
+
21
+ `saveFunnelEdits` can also take an inline `{ "referrer", "campaignId", "edits": [ { "screenId", "edit" } ] }` array instead of a `draftId` — a one-shot save with no persisted draft (you lose the preview step, so prefer the draft loop when the change is visual).
22
+
23
+ ### Domain rules
24
+ - **Base screens vs campaign screens.** A campaign-owned screen is edited in place; a **base/shared screen edited in a campaign context becomes a campaign *override*** — the shared screen is left untouched. `saveFunnelEdits` decides this automatically from the draft's `campaignId`, so a change scoped to one campaign only affects that campaign.
25
+ - **Read before every `update`.** Construct the edit from what `listFunnelScreens` returned (renderable ids are stable), never from memory.
26
+ - **One edit per `stageFunnelEdit`**, staged incrementally; each is validated as it lands.
27
+ - **Drift guard.** `saveFunnelEdits` rejects the promote if the live funnel changed since the draft was created — re-create the draft in that case.
28
+
29
+ ---
@@ -0,0 +1,10 @@
1
+ # Guests and members
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
+ **Reading is now available** via `searchUsers`. It returns a page of recent member activity — one event per member, each carrying the member's `serialNumber` plus the event (type, time, related object).
6
+
7
+ - Filter with `query` (free-text name), `eventTypes` (e.g. `sentText`, `receivedText`, `scan`, `order`, `rewardAwarded`, `rewardRedeemed`, `checkout`, the `*Attribution` types), `progressMinBound`/`progressMaxBound` (visit-count range), `isUnread: true` (members with unanswered inbound texts), `orderBy` (ASC|DESC by event time).
8
+ - Paginate with `limit` (default 100) and `cursor` (pass back the `cursor` from the previous call; an undefined cursor means no more pages).
9
+
10
+ **Replying by SMS is NOT exposed, deliberately.** The send primitive enforces opt-out, quiet-hours, dedup, and rate limits *downstream* (not at the endpoint), and opt-in is currently gated only by a UI control. If a reply capability is ever exposed, it must run with confirmation and must not bypass those guardrails. For now, tell the user that replying to guests is done in the app.
@@ -0,0 +1,25 @@
1
+ # Members program and wallet pass
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
+ ## Members program (retention)
7
+
8
+ The retention counterpart to campaigns — flows with no `campaignId`.
9
+
10
+ - **Automations are authorable** (see the automations workflow): use `listAutomationFlows` with `{ "scope": "membersProgram" }`, then the same create/edit loop. Remember the members-program 30-day `awardReward` default.
11
+ - **Rewards are now readable and creatable.** `listMembersProgramRewards` returns every reward with its catalog `name`, `staffInstructions`, and `pointsCost` (fields are `null` when unset). `createMembersProgramReward` takes `{ "name", "staffInstructions"?, "pointsCost"? }` and does the catalog-item dance for you: an existing catalog item with that exact name is reused (updating its `staffInstructions` if you passed any), otherwise one is created, then the reward row is linked to it.
12
+ - **The `pointsCost` fork matters.** A reward with `pointsCost` set is redeemed *by the member with points* and is never auto-awarded. Omit `pointsCost` for automation-awarded rewards — and then actually pair the reward with an `awardReward` automation (see the automations workflow), or it will never reach anyone. `listMembersProgramRewards` deliberately has no `hasAutomation` flag: cross-reference `listAutomations` for `awardReward` actions carrying the reward's `itemId` to find orphans.
13
+ - Reward **update/delete are not exposed** yet — those still need the app.
14
+
15
+ ---
16
+
17
+ ## Wallet pass configuration
18
+
19
+ The pass (the wallet membership card) is now CLI-readable and -writable as a whole document.
20
+
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
+ - Pass **image generation** (punch-card strips etc.) is not exposed — image workflows still need the app.
24
+
25
+ ---