@feastalytics/cli 0.1.15 → 0.1.17

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
@@ -1,105 +1,131 @@
1
1
  ---
2
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.
3
+ description: Operate a Feastalytics organization (campaigns, automations, funnels, members-program rewards, the wallet pass, creator sourcing, Meta ads, texting, onboarding, and read-only data queries) through the Feastalytics tools, either the `feast` CLI or the Feastalytics MCP server. 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", "text this guest back", "query my guests", "update the members program", or any request to script, batch or automate Feastalytics operations. Reach for it even when the user doesn't name the CLI or the MCP server: if the task is reading or changing Feastalytics data, these are the tools.
4
4
  ---
5
5
 
6
- # Feast CLI
6
+ # Feast
7
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.
8
+ Drive the Feastalytics platform with the same tool surface the in-app AI agent uses: campaigns, automations, funnels, members program, creator sourcing, Meta ads, texting, onboarding and data queries. Every tool hits the production API as the logged-in user.
9
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.
10
+ The live tool list is the source of truth for *which* tools exist and *what* they accept. Read it 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
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`.
12
+ ## Two ways in
13
13
 
14
- ## The core loop: discover → describe → call
14
+ The same tools, with the same names and inputs, are reachable two ways. Use whichever this session has.
15
15
 
16
- Don't guess tool names or input shapes. Introspect the live CLI:
16
+ | | MCP server | `feast` CLI |
17
+ |---|---|---|
18
+ | How you know you have it | Tools such as `listCampaigns` and `listOrganizations` are in your tool list | `feast tools` runs |
19
+ | See every tool | Your tool list | `feast tools` |
20
+ | One tool's input schema | Its entry in your tool list | `feast describe <tool>` |
21
+ | A schema that says "Named type X. Call describeSchema…" | Call `describeSchema` with `{ "names": ["X"] }` | `feast describe <tool>` prints it in full |
22
+ | Which organizations you can act on | `listOrganizations` | `feast whoami` |
23
+ | Choose the organization | The `organizationId` argument on each call | `--org <organizationId>` |
24
+ | Call a tool | Call it directly with its input | `feast call <tool> --org <id> --input '<json>'` (or `--input-file <path>`) |
17
25
 
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
- ```
26
+ Setting either one up (installing the CLI, logging in, connecting the MCP server) is in `references/setup.md`. Some environments hand you either one already connected and pinned to one organization; if a read such as `listCampaigns` works, you are set.
27
+
28
+ The rest of this skill and every workflow file names tools and their JSON input only. Translate to your transport with the table above.
29
+
30
+ ## The core loop: discover, read the schema, call
23
31
 
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.
32
+ Don't guess tool names or input shapes. Before calling an unfamiliar tool, read its input schema (and, over MCP, `describeSchema` for any named type it points to). The schema tells you the exact required fields. The CLI also validates input locally before sending, so a bad payload fails with the field named; over MCP the server validates and returns the same kind of error as a tool error.
25
33
 
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.
34
+ 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 the schema) wastes a message and never gets used. Only ask about what the call in front of you can actually configure.
27
35
 
28
36
  ## Organizations: never let the API guess
29
37
 
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.
38
+ Most tools act on one organization, and which one must be explicit. Acting on the wrong restaurant is worse than stopping to ask.
31
39
 
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.
40
+ - If the user names an organization, resolve it to its id (`listOrganizations` or `feast whoami`) and pass that id.
41
+ - If the user belongs to exactly one, it is used automatically.
42
+ - If they belong to several and you don't say which, the call is refused with a list of their organizations rather than silently picking one. Show the list and confirm which one they mean.
33
43
 
34
44
  ## Reads vs. writes
35
45
 
36
- Query tools (listing, describing, reading) are safe and read-only. Mutation tools (create, update, clone, delete, apply) change production data.
46
+ Query tools (listing, describing, reading) are safe and read-only; over MCP they carry a read-only hint. Mutation tools (create, update, clone, delete, apply, send) change production data.
37
47
 
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.
48
+ - Always name the organization explicitly for a mutation.
49
+ - On the CLI, a mutation prints the organization's name to stderr before it runs (`Acting on organization <id> as <ROLE>`), so a wrong `--org` shows up as the wrong restaurant. Read that line.
50
+ - **Nothing asks twice.** The CLI has no confirmation prompt, and an MCP client may or may not ask the user to approve a call depending on its settings. A mutation runs the moment it is called, and nothing undoes it.
41
51
 
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.)
52
+ That last point matters most for the tools that reach the real world rather than just the database:
43
53
 
44
- Prefer reading before writing: e.g. `listCampaigns` to find the right `campaignId` before `updateCampaign`, or `describe`/`listAutomationFlows` before creating a flow.
54
+ - `sendText` texts a guest or creator immediately, one person per call, with no scheduling and no undo.
55
+ - Approving or denying a creator visit (`updateCreatorVisit`) or deciding a submission (`decideCreatorSubmission`) texts that person. `updateCreatorVisit` can preview its texts with `dryRun: true` or skip them with `sideEffects: false`; `decideCreatorSubmission` can skip its text with `skipApprovalText`.
56
+ - Paying a creator's bonus (`createInfluencerPayout`) charges the organization's card.
57
+ - `awardReward` puts a real reward in a member's wallet pass, and a retried call grants a second one.
58
+ - `inviteUser` sends a real email.
59
+ - Buying a phone number bills the account.
60
+ - Publishing a campaign puts it live, and pricing a recurring promotion creates real Stripe products.
61
+ - Activating a Meta campaign spends real ad budget.
62
+ - Saving automation edits changes what guests receive.
63
+
64
+ Treat those as irreversible, and get the user's intent straight *before* the call. For a text, show the user the exact message and get their go-ahead first. The one schema-level gate is `publishAds`, which requires `confirm: true` in its input; that is you confirming, not anyone asking.
65
+
66
+ Prefer reading before writing: `listCampaigns` to find the right `campaignId` before `updateCampaign`, or `listAutomationFlows` before creating a flow.
45
67
 
46
68
  ## Building good input
47
69
 
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.
70
+ Input is a JSON object built from the tool's schema. When a tool references another entity by id (a campaign id, location id, flow id), look that id up first with the relevant list or read tool rather than inventing it.
49
71
 
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.
72
+ For the domain meaning of fields (how automations chain, what a funnel screen contains, how offers are structured), consult `references/domains.md` when the schema alone isn't enough.
51
73
 
52
74
  ## Workflows
53
75
 
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.
76
+ 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 and ids first, then act" shape recurs across campaigns, funnels and offers.
55
77
 
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.
78
+ **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, and neither is in the tool schemas. Read it first; don't reconstruct the sequence from tool descriptions.
57
79
 
58
80
  | Doing this | Read |
59
81
  |---|---|
60
82
  | 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` |
83
+ | Anything touching automations: creating, editing, simulating, promoting a draft | `references/workflows/automations.md` |
62
84
  | Editing funnel screens, applying a funnel template, staging a new screen | `references/workflows/funnels.md` |
63
85
  | Writing guest-facing Meta ad copy (`adCopy`) | `references/workflows/ad-copy-guest.md` |
64
86
  | Writing creator-recruitment ad copy (`recruitmentAdCopy`) | `references/workflows/ad-copy-creator.md` |
65
87
  | 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` |
88
+ | Creator sourcing: approving applicants, reviewing content, creatives, payouts, reimbursements, texting a creator | `references/workflows/creators.md` |
89
+ | Members-program rewards, granting a reward to one member; reading or saving the wallet pass configuration | `references/workflows/members-program.md` |
68
90
  | 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` |
91
+ | Searching guests and members, texting a guest back, querying anything through the data catalog | `references/workflows/guests.md` |
70
92
 
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.
93
+ 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
94
 
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.
95
+ 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
96
 
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.
97
+ 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
98
 
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.
99
+ Some things have no tool: 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 tool that isn't in the tool list; tell the user that part has to be done in the dashboard.
78
100
 
79
101
  ## Link to what you touched
80
102
 
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.
103
+ Work you do through these tools 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. 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
104
 
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.
105
+ **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
106
 
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.
107
+ 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 suppress analytics versus merely tagging a visit as a preview.
86
108
 
87
109
  ## Worked example
88
110
 
89
111
  User: "add a Free Dessert reward members can redeem for 100 points in my Plum Vietnamese org."
90
112
 
113
+ 1. Find the organization's id with `listOrganizations` (CLI: `feast whoami`), unless you were given it.
114
+ 2. Read the `createMembersProgramReward` schema to learn the input shape (`type: "item"` vs `type: "name"`).
115
+ 3. Call `listMembersProgramRewards` to avoid duplicating an existing reward or catalog item.
116
+ 4. Call `createMembersProgramReward` with `{ "type": "name", "name": "Free Dessert", "pointsCost": 100 }`.
117
+
118
+ On the CLI, step 4 is:
119
+
91
120
  ```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}'
121
+ feast call createMembersProgramReward --org <organizationId> --input '{"type":"name","name":"Free Dessert","pointsCost":100}'
96
122
  ```
97
123
 
98
- The pattern generalizes: identify the org, learn the tool, resolve any referenced ids, then act.
124
+ The pattern generalizes: identify the organization, learn the tool, resolve any referenced ids, then act.
99
125
 
100
126
  ## When something fails
101
127
 
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.
128
+ - Not logged in, session expired, `feast` isn't on PATH, or the MCP server asks you to authenticate: `references/setup.md`.
129
+ - "You belong to multiple organizations": pick one and pass it explicitly (see Organizations above).
130
+ - The input doesn't match the schema: re-read the tool's schema (and any named type) and fix the named fields.
131
+ - A tool you expected isn't in the tool list: don't fabricate a call; tell the user that part has to be done in the dashboard.
@@ -1,10 +1,10 @@
1
1
  # Feastalytics domain model
2
2
 
3
- Background for constructing tool input correctly. This is the conceptual map; the authoritative field list for any tool always comes from `feast describe <tool>`.
3
+ Background for constructing tool input correctly. This is the conceptual map; the authoritative field list for any tool always comes from its input schema.
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 `getOrganization` (it returns the org's `locations`) — not the organization id.
7
+ The top-level tenant. Nearly every tool is scoped to one organization (see Organizations in `SKILL.md`). 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
 
@@ -13,7 +13,9 @@ A campaign is an acquisition effort. It bundles:
13
13
  - **automations** (see below) scoped to that campaign,
14
14
  - **promotions/offers** attached to it.
15
15
 
16
- Typical flow: `createCampaign` (set `isCreating: true` if you'll finish it with `populateCampaign`), then `populateCampaign`, then choose a funnel template. `cloneCampaign` duplicates an existing one (funnel + automations + offers) — it needs the source campaign id and a `referrer` (a subdomain from the org's `subdomains2`).
16
+ `listCampaigns` resolves a `campaignId`: use each summary's `id` (a UUID), not the nested Meta campaign id. Summaries also carry the name, `shorthand` (used in reservation links), publish state and referrers; `getCampaign` has the full configuration.
17
+
18
+ Typical flow: `createCampaign`, then always `populateCampaign` (a new campaign stays hidden from the dashboard until it is populated), then choose a funnel template. `cloneCampaign` duplicates an existing one (funnel, automations and offers); it needs the source campaign id and a `referrer` (a subdomain from the org's `subdomains2`).
17
19
 
18
20
  ## Automations and flows
19
21
 
@@ -29,16 +31,16 @@ Promotions live on the campaign record (`getCampaign` / `updateCampaign`), and r
29
31
 
30
32
  ## Members program (retention)
31
33
 
32
- The retention counterpart to campaigns: rewards and pass configuration for returning guests. Members-program automations are the flows with no `campaignId` (`scope: "membersProgram"` above). Rewards are fully manageable (`listMembersProgramRewards` / `createMembersProgramReward` / `updateMembersProgramReward` / `deleteMembersProgramReward`), and the wallet pass is a read-modify-write document (`getPassConfiguration` / `updatePassConfiguration`) — see `workflows/members-program.md`.
34
+ The retention counterpart to campaigns: rewards and pass configuration for returning guests. Members-program automations are the flows with no `campaignId` (`scope: "membersProgram"` above). Rewards are fully manageable (`listMembersProgramRewards` / `createMembersProgramReward` / `updateMembersProgramReward` / `deleteMembersProgramReward`), and the wallet pass is a read-modify-write document (`getPassConfiguration` / `updatePassConfiguration`). See `workflows/members-program.md`.
33
35
 
34
36
  ## Creator sourcing
35
37
 
36
- Restaurants recruit local content creators to visit and post. One config per location (`getInfluencerBoardConfig`), an approval queue of applications, content review, and bonus payouts — see `workflows/creators.md`. Recruitment *ads* publish through the Meta ads surface (`workflows/ads.md`).
38
+ Restaurants recruit local content creators to visit and post. One config per location (`getInfluencerBoardConfig`), an approval queue of applications, content review, and bonus payouts. See `workflows/creators.md`. Recruitment *ads* publish through the Meta ads surface (`workflows/ads.md`).
37
39
 
38
40
  ## Meta ads
39
41
 
40
- A template-driven publish pipeline: `listAdTemplates` → `planAds` → `publishAds` → `getJob` → `setAdCampaignStatus`, plus `ads_*` tools for reading and steering what's already on the ad account — see `workflows/ads.md`.
42
+ A template-driven publish pipeline: `listAdTemplates` → `planAds` → `publishAds` → `getJob` → `setAdCampaignStatus`, plus `ads_*` tools for reading and steering what's already on the ad account. See `workflows/ads.md`.
41
43
 
42
44
  ## The data catalog
43
45
 
44
- `describeData` / `queryData` expose a read-only, org-scoped query surface over the data model — guests, orders, menu items, texts, creator visits, payouts. When no purpose-built tool answers a read question, the catalog usually does; `describeData` with no arguments is the index.
46
+ `describeData` / `queryData` expose a read-only, org-scoped query surface over the data model: guests, orders, menu items, texts, creator visits, payouts. When no purpose-built tool answers a read question, the catalog usually does; `describeData` with no arguments is the index.
@@ -1,16 +1,16 @@
1
1
  # Linking to your work
2
2
 
3
- Most of what you create or change through the CLI has a stable URL in the product. Handing one over is cheap and saves the user hunting through the dashboard for the thing you just made — opening it is usually their next step anyway.
3
+ Most of what you create or change through these tools has a stable URL in the product. Handing one over is cheap and saves the user hunting through the dashboard for the thing you just made; opening it is usually their next step anyway.
4
4
 
5
- So offer links freely: after a turn where you created, changed, or published something, close with a short markdown list — usually two to four, covering where to see it, where to edit it, and where to preview it. When someone asks where a thing lives or how to set it up, lead with the link rather than describing where to click. It's an affordance, not a checkpoint; nobody has to go verify your work.
5
+ So offer links freely: after a turn where you created, changed, or published something, close with a short markdown list, usually two to four, covering where to see it, where to edit it, and where to preview it. When someone asks where a thing lives or how to set it up, lead with the link rather than describing where to click. It's an affordance, not a checkpoint; nobody has to go verify your work.
6
6
 
7
- You can build almost every URL below from ids you already have. `<organizationId>` is the same value you pass to `--org`. Campaign, flow and draft ids come back from the tool call you just made. Only `<subdomain>` needs a lookup.
7
+ You can build almost every URL below from ids you already have. `<organizationId>` is the organization id you pass on every call. Campaign, flow and draft ids come back from the tool call you just made. Only `<subdomain>` needs a lookup.
8
8
 
9
- Angle brackets below mark a value **you** substitute. A finished link contains no brackets, no braces and no backticks — if you emit `{{...}}` or a bare `<campaignId>`, the link is broken.
9
+ Angle brackets below mark a value **you** substitute. A finished link contains no brackets, no braces and no backticks. If you emit `{{...}}` or a bare `<campaignId>`, the link is broken.
10
10
 
11
11
  ## Dashboard
12
12
 
13
- Everything an authenticated user sees hangs off `https://feastalytics.com/<organizationId>/app`. Note the shape: the organization id is a **path segment**, not a subdomain — there is no `app.feastalytics.com`.
13
+ Everything an authenticated user sees hangs off `https://feastalytics.com/<organizationId>/app`. Note the shape: the organization id is a **path segment**, not a subdomain. There is no `app.feastalytics.com`.
14
14
 
15
15
  ```
16
16
  / home
@@ -31,22 +31,22 @@ Members-program panels: `overview`, `funnel`, `automations`, `pass-builder`, `re
31
31
 
32
32
  Settings tabs: `account`, `general`, `members`, `integrations`, `notifications`, `usage`, `scanning`, `texting`, `subscription`. Point people at `/settings/integrations` when a task needs a POS or Meta connection you can't make for them.
33
33
 
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.
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
36
  ### Onboarding task pages
37
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.
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
39
 
40
40
  ### Automation previews
41
41
 
42
- These two hang off the **root**, not off `/<organizationId>/app` — the organization id is the first path segment:
42
+ These two hang off the **root**, not off `/<organizationId>/app`. The organization id is the first path segment:
43
43
 
44
44
  ```
45
45
  https://feastalytics.com/automation-preview/<organizationId>/<flowId>
46
46
  https://feastalytics.com/automation-preview/<organizationId>/<flowId>?draftId=<draftId>
47
47
  ```
48
48
 
49
- Without `draftId` it dry-runs the live flow as a text-message thread. With one, the same page diffs the draft's staged changes against live — added messages tinted, removed struck through, edited showing the old copy above the new — which is the link to hand someone before you promote.
49
+ Without `draftId` it dry-runs the live flow as a text-message thread. With one, the same page diffs the draft's staged changes against live (added messages tinted, removed struck through, edited showing the old copy above the new), which is the link to hand someone before you promote.
50
50
 
51
51
  A `flowId` contains `:` and `;` and **must be percent-encoded** in the path. Easier: `createAutomationDraft` and `stageAutomationEdits` both return `previewUrls`, already built and encoded, one per flow the draft touches. Use those rather than assembling your own.
52
52
 
@@ -73,10 +73,10 @@ The `/preview/<draftId>` route is the payoff of the draft → preview → promot
73
73
 
74
74
  ## Worked example
75
75
 
76
- After creating a campaign and applying a funnel template. Every id below is substituted — this is what a finished message looks like, with nothing left to fill in:
76
+ After creating a campaign and applying a funnel template. Every id below is substituted. This is what a finished message looks like, with nothing left to fill in:
77
77
 
78
78
  ```markdown
79
- Done — "Fall Prix Fixe" is live as a draft.
79
+ Done. "Fall Prix Fixe" is live as a draft.
80
80
 
81
81
  - [Open the campaign](https://feastalytics.com/3e8cb27c-6e54-444b-859f-66dbae0e711b/app/campaigns/e8ccc852-6555-4a9a-b48b-127d687bb34a)
82
82
  - [Edit the funnel](https://feastalytics.com/3e8cb27c-6e54-444b-859f-66dbae0e711b/app/campaigns/e8ccc852-6555-4a9a-b48b-127d687bb34a?panel=funnel-v2)
@@ -1,31 +1,45 @@
1
1
  # Setup, auth, and organizations
2
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.
3
+ One-time and troubleshooting material: connecting the Feastalytics MCP server or installing and logging in to the `feast` CLI, keeping the CLI 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
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.
5
+ Some environments hand you the tools already connected and authenticated, pinned to a single organization. Nothing in this file applies there: if a read such as `listCampaigns` works and your calls are going to the right restaurant, you are already set up.
6
6
 
7
- ## Installing
7
+ ## The MCP server
8
+
9
+ The hosted server is at `https://mcp.feast-api.com/mcp` (streamable HTTP, OAuth). The user adds it once in their MCP client, for example:
10
+
11
+ ```bash
12
+ claude mcp add --transport http feast https://mcp.feast-api.com/mcp # Claude Code
13
+ ```
14
+
15
+ In other clients (Claude Desktop, claude.ai, Cursor) it is a custom connector with that URL. The first call opens a browser to the Feastalytics login, then a consent page naming the client; after that the client refreshes the session itself. A session lasts 30 days from that login, then the user logs in again. If tool calls start failing with an authentication error, ask the user to reconnect the server in their client.
16
+
17
+ You can't add the server for the user from inside a conversation. Tell them the URL and where to add it.
18
+
19
+ ## The `feast` CLI
20
+
21
+ ### Installing
8
22
 
9
23
  The `feast` CLI must be installed and on PATH:
10
24
 
11
25
  ```bash
12
- npm install -g @feastalytics/cli # or run ad-hoc with: npx @feastalytics/cli <command>
26
+ npm install -g @feastalytics/cli # or run ad hoc with: npx @feastalytics/cli <command>
13
27
  ```
14
28
 
15
- If the global install fails on permissions, don't retry with `sudo` — tell the user and fall back to `npx @feastalytics/cli@latest`.
29
+ If the global install fails on permissions, don't retry with `sudo`. Tell the user and fall back to `npx @feastalytics/cli@latest`.
16
30
 
17
- ## Authenticating
31
+ ### Authenticating
18
32
 
19
- Authenticate once — tokens are cached in `~/.config/feast-cli/credentials.json` and refreshed automatically:
33
+ Authenticate once. Tokens are cached in `~/.config/feast-cli/credentials.json` and refreshed automatically:
20
34
 
21
35
  ```bash
22
36
  feast login # opens a browser to authorize (default)
23
- feast login --password [username] # headless / CI: username + password prompt, no browser
37
+ feast login --password [username] # headless or CI: username and password prompt, no browser
24
38
  ```
25
39
 
26
- If a command reports you're not logged in or the session expired, re-run `feast login`.
40
+ If a command reports you're not logged in or the session expired, run `feast login` again.
27
41
 
28
- ## Staying current
42
+ ### Staying current
29
43
 
30
44
  Neither the CLI nor this skill updates itself. When a command prints an update notice on stderr:
31
45
 
@@ -36,24 +50,31 @@ Update available: feast 0.1.1 → 0.2.0
36
50
  update both, then tell the user in one line that you did:
37
51
 
38
52
  ```bash
39
- npm install -g @feastalytics/cli@latest # only if `feast` is already on PATH from a global install
53
+ npm install -g @feastalytics/cli@latest # only if `feast` is on PATH from a global install
40
54
  npx skills add feastalytics/cli -g -a '*' -y # refresh this skill from the repo
41
55
  ```
42
56
 
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.
57
+ 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
58
 
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.
59
+ 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. The MCP server always serves the current tools, so over MCP only the skill needs refreshing.
46
60
 
47
- ## Which organization
61
+ ## Playbook skills
48
62
 
49
- Most tools act on one organization. A user often belongs to several, so which one you target matters and must be explicit.
63
+ Feastalytics publishes further skills that build on this one (campaign diagnosis and other playbooks). They come from the API, not from GitHub, and installing them takes the CLI:
50
64
 
51
65
  ```bash
52
- feast whoami # shows the logged-in user and every org (with names) they can act on
66
+ feast skill list # what is published for your organization
67
+ feast skill install feast-playbooks # install or refresh one into ~/.claude/skills/
53
68
  ```
54
69
 
55
- Pass the target org with `--org <organizationId>`:
70
+ 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.
71
+
72
+ ## Which organization
73
+
74
+ Most tools act on one organization. A user often belongs to several, so which one you target matters and must be explicit.
56
75
 
57
- - If the user names an org, resolve it to its id with `feast whoami` and pass that id.
58
- - If the user belongs to exactly one org, the CLI uses it automatically — no flag needed.
59
- - 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.
76
+ - See every organization the user can act on, with names and their role: `listOrganizations` over MCP, `feast whoami` on the CLI.
77
+ - Pass the target as the `organizationId` argument (MCP) or `--org <organizationId>` (CLI).
78
+ - If the user names an organization, resolve it to its id from that list and pass the id.
79
+ - If the user belongs to exactly one organization, it is used automatically.
80
+ - If they belong to more than one and you don't pass one, the call is refused with the list of their organizations rather than silently picking one. That's intentional: acting on the wrong organization is worse than stopping to ask. Show the user the list and confirm which one they mean.
@@ -1,45 +1,43 @@
1
1
  # Creator-recruitment ad copy (`recruitmentAdCopy`)
2
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
3
  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
4
 
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.
5
+ **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. That is 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
6
 
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`.
7
+ 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
8
 
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.
9
+ **You write the copy yourself.** The dashboard has a "generate copy" button behind an LLM call; there is no tool for it and you shouldn't want one, because it would be you calling an 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
10
 
13
11
  ## Variations: write a set, not a single
14
12
 
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.
13
+ 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
14
 
17
15
  **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
16
 
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.
17
+ 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
18
 
21
19
  ## The audience
22
20
 
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.
21
+ **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
22
 
25
- ## Absolute rules — this is exactly where past generations went wrong
23
+ ## Absolute rules: this is exactly where past generations went wrong
26
24
 
27
25
  - **Never mention an offer, deal, voucher, promotion, discount, "claiming" anything, or pre-paying.** This is a collaboration, not a customer offer.
28
26
  - **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.
27
+ - **No customer-facing language**: "claim your voucher", "come hungry", "limited time offer", "this week only", "tap below to save".
28
+ - **Don't reuse the campaign's guest-facing framing**: banner copy, promotions, offer headlines. None of it belongs here, however good it is.
31
29
  - **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
30
 
33
31
  ## What to pitch
34
32
 
35
33
  - 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.
34
+ - **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.
35
+ - **The creator gives:** one 30 to 60 second vertical reel (Instagram Reel / TikTok), filmed during the visit, submitted within 72 hours.
38
36
  - **Eligibility:** an active food/lifestyle creator with a minimum local-area follower count on Instagram or TikTok.
39
37
 
40
- ## Angles — rotate across the set
38
+ ## Angles: rotate across the set
41
39
 
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.
40
+ 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
41
 
44
42
  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
43
 
@@ -49,7 +47,7 @@ Take as many of these as the collab genuinely supports rather than filling a quo
49
47
 
50
48
  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
49
 
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.
50
+ Specifics over fluff: name the dollar amounts, the deliverable (one reel, 30 to 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
51
 
54
52
  **GOOD primary text:**
55
53
 
@@ -58,36 +56,36 @@ Plum Vietnamese is booking local food creators this month. 📸
58
56
 
59
57
  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
58
 
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.
59
+ 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
60
  ```
63
61
 
64
- **GOOD headlines:** `Get paid to post about Plum` · `Local creators — eat free, post a reel` · `Foodies w/ 1,000+ followers, read this 👀`
62
+ **GOOD headlines:** `Get paid to post about Plum` · `Local creators: eat free, post a reel` · `Foodies w/ 1,000+ followers, read this 👀`
65
63
 
66
- **BAD — do not generate this:**
64
+ **BAD (do not generate this):**
67
65
 
68
66
  ```
69
- Free meal at Plum Vietnamese this week! Claim your voucher and come hungry — you won't want to miss this deal. 🍴
67
+ Free meal at Plum Vietnamese this week! Claim your voucher and come hungry, you won't want to miss this deal. 🍴
70
68
  ```
71
69
 
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.
70
+ 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
71
 
74
- ## The terms are baked into the copy — record them
72
+ ## The terms are baked into the copy: record them
75
73
 
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.
74
+ `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
75
 
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.
76
+ **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
77
 
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.
78
+ 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
79
 
82
80
  ## Saving it
83
81
 
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.
82
+ One `updateCampaign` call writes `recruitmentAdCopy`. Read `updateCampaign`'s input schema 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
83
 
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.
84
+ **The one thing the schema won't tell you: `update.recruitmentAdCopy` 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
85
 
88
86
  ## Publishing
89
87
 
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.
88
+ **Drivable through the tools; 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
89
 
92
90
  ---
93
91