@feastalytics/cli 0.1.19 → 0.1.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +3933 -9787
- package/package.json +1 -1
- package/plugin/skills/feast/references/domains.md +2 -2
- package/plugin/skills/feast/references/workflows/ads.md +9 -8
- package/plugin/skills/feast/references/workflows/automations.md +4 -6
- package/plugin/skills/feast/references/workflows/campaigns.md +20 -15
- package/plugin/skills/feast/references/workflows/creators.md +3 -3
- package/plugin/skills/feast/references/workflows/members-program.md +1 -1
- package/plugin/skills/feast/references/workflows/onboarding.md +3 -3
- package/plugin/skills/feast/references/workflows/videos.md +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@feastalytics/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.21",
|
|
4
4
|
"description": "Command-line client for the Feastalytics platform — list, create, and update campaigns, automations, offers, and members-program rewards from the terminal.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -15,14 +15,14 @@ A campaign is an acquisition effort. It bundles:
|
|
|
15
15
|
|
|
16
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
17
|
|
|
18
|
-
Typical flow: `createCampaign`, then
|
|
18
|
+
Typical flow: `createCampaign`, then `updateCampaign` with `isCreating: false` to finish setup, then a funnel template and automations (`workflows/campaigns.md`). `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`).
|
|
19
19
|
|
|
20
20
|
## Automations and flows
|
|
21
21
|
|
|
22
22
|
- An **automation** is one trigger → action unit (e.g. "on checkout, award reward").
|
|
23
23
|
- A **flow** is a named grouping of automations. A flow belongs to *either* a campaign *or* the members program (never both).
|
|
24
24
|
- `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.
|
|
25
|
-
- **Authoring:** `createAutomationFlow` makes a flow; `
|
|
25
|
+
- **Authoring:** `createAutomationFlow` makes a flow; automations are created, updated and deleted through a draft (`createAutomationDraft` → `stageAutomationEdits` → `simulateAutomationDraft` → `saveAutomationEdits`; create ops **require** a `flowId`); `updateAutomationFlow` / `deleteAutomationFlow` manage the flow itself. See `workflows/automations.md` for the ordering and the trigger/condition/send-time rules.
|
|
26
26
|
- Templates: `listAutomationTemplates` → `listTemplateAutomations` (preview) → `applyAutomationTemplate`. Only apply a template to a campaign/members-program that has no existing flows.
|
|
27
27
|
|
|
28
28
|
## Offers and promotions
|
|
@@ -14,11 +14,12 @@ For the *words* in the ads, read the copywriting file for your audience first: `
|
|
|
14
14
|
### The loop
|
|
15
15
|
|
|
16
16
|
1. `listAdTemplates`: pick the template, read each variable's `producedBy`.
|
|
17
|
-
2. Gather variables with those tools
|
|
18
|
-
- `
|
|
19
|
-
- `
|
|
20
|
-
- `
|
|
21
|
-
- `
|
|
17
|
+
2. Gather variables with those tools. Most Meta ids come from one call, `ads_get_assets`, with an `include` list naming the sections you need; it runs them in parallel and returns only those keys. Then `listCreatives`, `getCampaign`, etc. A typical first call is `{ "include": ["adAccounts", "pages"] }`; once you have the ids, `{ "include": ["instagramAccounts", "instagramMedia", "customAudiences"], "pageId": "...", "adAccountId": "..." }`. A section missing its id (`adAccountId` for `customAudiences` and `datasets`, `pageId` for `instagramAccounts` and `instagramMedia`) is refused.
|
|
18
|
+
- `adAccounts` are the accounts you can publish to (`hiddenAdAccountCount` says how many the token reaches beyond them; `includeUnassigned: true` lists those for diagnosing a missing account, and they are not publishable).
|
|
19
|
+
- `pages`: prefer a Page with `usedByOrganization: true`; the token reaches other businesses' Pages and nothing stops you publishing from the wrong one. Each Page shows its linked Instagram business account when it has one.
|
|
20
|
+
- `customAudiences` supplies the `customAudienceIds` and `excludedCustomAudienceIds` variables. Skip any audience with `isReadyForUse: false` (Meta will not deliver to it, so an ad set targeting it reaches nobody), and remember audience ids belong to one ad account and are rejected by another. An audience's size bounds are approximate and go stale while Meta is updating it. Nothing says how fresh the underlying list is: a customer-list audience is a snapshot of the last upload, and `timeUpdated` is when that happened.
|
|
21
|
+
- `instagramMedia` is `{ instagramAccount, media }`: up to 50 recent posts (id, caption, thumbnail, mediaType, permalink, timestamp) from the Instagram business account linked to that Page, each with the `instagramUserId` that goes on an `igMedia` creative reference. A Page with no linked Instagram business account returns `instagramAccount: null`.
|
|
22
|
+
- `instagramAccounts` is the full list of identities that Page can run ads as, for the `instagramAccountId` variable (`kind: "pageBacked"` is the identity Meta creates for a Page with no Instagram account, and is valid too).
|
|
22
23
|
3. `planAds`: fix every issue with severity `error` and re-plan. Summarize the resulting tree (campaign name, budget, targeting, ad count) for the user before going further; the plan is the thing they're approving.
|
|
23
24
|
4. `publishAds` with the returned `variables`, `overrides` and `planHash` unchanged, plus:
|
|
24
25
|
- `confirm: true`. The schema demands it; this is the only tool with a schema-level confirm.
|
|
@@ -26,7 +27,7 @@ For the *words* in the ads, read the copywriting file for your audience first: `
|
|
|
26
27
|
- `effects`: see below.
|
|
27
28
|
|
|
28
29
|
If the organization resolves differently than when you planned (a `PLAN_STALE` refusal), re-run `planAds` and show the human what changed before publishing again.
|
|
29
|
-
5.
|
|
30
|
+
5. Pass `"wait": true` to `publishAds` and it returns the finished `job` (up to 20 seconds). If that `job` is still `PENDING` or `RUNNING`, call `getJob` with the `jobId` + `jobType` and `"wait": true` until `COMPLETED` or `FAILED`. `{ "job": null }` means not landed yet, so call again. **Read the job's effect outcomes.** Each declared effect reports `done`, `skipped` or `error` with a human-readable detail, and effect failures do not fail the job (the ads already exist by then), so this is the only place you find out.
|
|
30
31
|
6. `setAdCampaignStatus` to go live, after the user says go. Check the preflight counts in the response. For guest-facing ads linked to a Feast campaign, run the campaign readiness check first (`getTaskboard` with the campaign scope, see "Before a campaign goes live" in `campaigns.md`). Ads that send traffic to a funnel with no automations pay for sign ups that never receive their offer.
|
|
31
32
|
|
|
32
33
|
### Effects: the write-back is declared, not called afterwards
|
|
@@ -50,9 +51,9 @@ An effect that reports `error` in the job is a case for the dashboard, not for p
|
|
|
50
51
|
|
|
51
52
|
### Reading and steering what's live
|
|
52
53
|
|
|
53
|
-
- `ads_get_ad_entities`: read campaigns/ad sets/ads on an account, creatives attached. The diagnostic read for everything below. `level` says what comes back; an id at the requested level fetches that one object, and an id from a level above lists that object's children: `campaignId` with `level: "adSet"` returns that campaign's ad sets, and with `level: "ad"` every ad in it across all its ad sets. Where several ids apply, the narrowest wins.
|
|
54
|
+
- `ads_get_ad_entities`: read campaigns/ad sets/ads on an account, creatives attached. The diagnostic read for everything below. `level` says what comes back; an id at the requested level fetches that one object, and an id from a level above lists that object's children: `campaignId` with `level: "adSet"` returns that campaign's ad sets, and with `level: "ad"` every ad in it across all its ad sets. Where several ids apply, the narrowest wins. To see the whole tree, do not walk it one level per call: pass `includeChildren: true` and each campaign comes back with its `adSets`, each with its `ads` (at `level: "adSet"`, each ad set with its `ads`). `limit` and `effectiveStatus` apply to every level, so add a `campaignId` when you need one campaign's complete tree.
|
|
54
55
|
- `ads_update_entity`: rename, re-budget, or pause; moving a daily budget is how you scale a winner or throttle a loser. Budgets are integer cents and **replace** the current value; read first, confirm the number with the human. Creatives are immutable at Meta, so new copy or media means a new ad (the `addAds` template).
|
|
55
56
|
- `ads_activate_entity`: go-live for structures Feastalytics did *not* publish. No cascade: activate top-down and check `willDeliver`; a child under a paused parent is live in name only. For campaigns Feastalytics published, `setAdCampaignStatus` cascades and is the right tool: those are published paused at all three levels, so activating the campaign alone would spend nothing.
|
|
56
|
-
- `
|
|
57
|
+
- `ads_get_assets` with `include: ["datasets"]` and an `adAccountId` / `ads_create_dataset`: pixel checks and creation. The pixel a campaign should optimise against is the one its funnel actually fires (from the layout config), not whichever pixel looks plausible on the account. After creating one, write its id back with `updateBrandIdentity`; creation alone connects nothing. That layout config value is what makes the funnel fire the pixel and what the onboarding task reads.
|
|
57
58
|
|
|
58
59
|
> **Not exposed:** ad-copy generation (write it yourself: `ad-copy-guest.md` / `ad-copy-creator.md`), creative *content* editing on Meta (immutable there), and publishing creator content as partnership ads.
|
|
@@ -16,14 +16,14 @@ Automations have a staging tier, and it is the default path. Changes accumulate
|
|
|
16
16
|
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.
|
|
17
17
|
3. `listAutomations` with `{ "flowId": "<id>" }` to see the automations already in that flow before editing (omit the input to list every automation in the org). **Do this immediately before every `update` op you stage, not just once at the start of the session.** An `update` replaces an automation's entire `actions` array; it does not merge one action in. If you reconstruct `actions` from an earlier tool result or from what you remember discussing, rather than the automation's current live state, you silently drop whatever isn't in your reconstruction (a reward grant, a task action, anything not under discussion in that turn). This is true even a few messages later in the same conversation, once the user has asked for a second or third change to the same automation.
|
|
18
18
|
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.**
|
|
19
|
-
5. `stageAutomationEdits` with `{ "draftId": "<id>", "operations": [...] }`. Each operation is the named type `AutomationOperation
|
|
19
|
+
5. `stageAutomationEdits` with `{ "draftId": "<id>", "operations": [...] }`. Each operation is the named type `AutomationOperation`, and its automation fields use the named types `UserCondition`, `AutomationTrigger`, `AutomationAction`, `AutomationSendTime` and `AutomationVariant`. Fetch those shapes once before writing your first op:
|
|
20
20
|
- `{ "type": "create", "automation": { "automationId": "<new-uuid>", "flowId": "<id>", "title": "...", "isActive": true, "triggers": [...], "conditions": [...], "actions": [...], "time": {...} } }`: generate a fresh UUID for `automationId` (the key is `automationId`, not `id`). `automationId`, `isActive`, `triggers`, `conditions`, `actions` and `time` are required; set the `flowId` and a descriptive title too. Create ops require the flowId.
|
|
21
21
|
- `{ "type": "update", "automationId": "<id>", "automation": { ...changed fields... } }`
|
|
22
22
|
- `{ "type": "delete", "automationId": "<id>" }`: blocked at save time if the automation already has sends.
|
|
23
23
|
- `{ "type": "createVariant", "automationId": "<id>", "variantId": "<new-uuid>", "variant": <AutomationVariant> }` and `{ "type": "updateVariant", "automationId": "<id>", "variantId": "<id>", "variant": { "triggers"?, "conditions"?, "actions"?, "time"? } }` add or change an A/B variant of an automation.
|
|
24
24
|
Call it repeatedly to build a change up; ops append in order.
|
|
25
|
-
6. `
|
|
26
|
-
- **First call:**
|
|
25
|
+
6. `simulateAutomationDraft` with `{ "draftId": "<id>" }`: dry-run the draft's staged operations against a synthetic event timeline with **no real sends** and confirm the right automations fire. `flowId` defaults to the draft's flow when it touches exactly one; pass it when the draft touches several. If the simulation surprises you, stage a correction rather than promoting and patching live.
|
|
26
|
+
- **First call:** omit `events`. The server seeds a timeline from that flow's triggers (a `viewCampaign` event when the flow has a campaign, then the first eligible trigger event 15 seconds later) and returns it as `eventsUsed`.
|
|
27
27
|
- **Later calls:** to test another day or continue the guest's journey, change `at` on those events or append more, and pass the array back as `events`. Each event is `{ "type": "...", "at": "<ISO 8601 timestamp>" }` plus a few optional fields per type; the server fills in the guest, organization and campaign.
|
|
28
28
|
- The result is `scheduledTexts` (what would be sent, and when) plus `eventsUsed`.
|
|
29
29
|
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.
|
|
@@ -31,11 +31,9 @@ Automations have a staging tier, and it is the default path. Changes accumulate
|
|
|
31
31
|
|
|
32
32
|
`discardAutomationDraft` throws a draft away without promoting. `getAutomationDraft` re-reads one by id, including `resultingAutomations` (each touched automation as it will look after the save); check it for fields that changed or disappeared. Drafts expire after 14 days.
|
|
33
33
|
|
|
34
|
-
**`batchEditAutomations` 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.
|
|
35
|
-
|
|
36
34
|
`updateAutomationFlow` renames/retitles a flow; `deleteAutomationFlow` removes a flow and its automations (blocked at ≥20 sends; turn it off instead).
|
|
37
35
|
|
|
38
|
-
> **Not exposed:** actually *firing* an automation at a live member (the app's "run") is intentionally not a tool, because it sends a real SMS. Use `
|
|
36
|
+
> **Not exposed:** actually *firing* an automation at a live member (the app's "run") is intentionally not a tool, because it sends a real SMS. Use `simulateAutomationDraft` for verification; real sends happen in the app.
|
|
39
37
|
|
|
40
38
|
### Choosing the trigger
|
|
41
39
|
|
|
@@ -2,18 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
## Creating a campaign
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
1.
|
|
8
|
-
2.
|
|
9
|
-
3.
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
5
|
+
Ask the dashboard wizard's questions, one at a time, skipping any the user already answered:
|
|
6
|
+
|
|
7
|
+
1. Name, and which site (`subdomains2[].subdomain` from `getOrganization`).
|
|
8
|
+
2. "How will you run this campaign?": I have the content (`self`), I need creator-made content (`creator`, only if `featureFlags.isDfyEnabled`), or tracking only.
|
|
9
|
+
3. "Is there an offer?"
|
|
10
|
+
4. If yes: its name, whether guests can prepay (then a price in dollars), and an image (required for prepay). Upload with `getMediaUploadUrl` (`scope: "organizationFilePublic"`, a PNG), PUT the bytes, keep the `key`.
|
|
11
|
+
|
|
12
|
+
Then:
|
|
13
|
+
|
|
14
|
+
1. `createCampaign` with `{ "campaign": { "name": "...", "referrers": ["<subdomain>"] } }`. Keep the returned id.
|
|
15
|
+
2. One `updateCampaign` with `"isCreating": false` (until then the campaign is stuck in the creation wizard) and:
|
|
16
|
+
- `enabledFeatures`: start from `["OFFER", "CREATOR_SOURCING", "AD_PUBLISHING", "FUNNEL", "AUTOMATIONS"]`; drop `OFFER` with no offer, drop `CREATOR_SOURCING` for `self`. Tracking only: `[]` and `"isPublished": true`.
|
|
17
|
+
- With an offer: `promotions: [{ "promoId": "<new-uuid>", "type": "basic", "basic": { "title": "...", "price"?: 29.99, "canPrePay"?: true } }]`, `imageUrl: { "type": "s3", "key": "..." }`, and if there is no description, `"Prepay for <offer>"` or `"Earn <offer>"`.
|
|
18
|
+
|
|
19
|
+
Tracking only stops here. Otherwise continue:
|
|
20
|
+
|
|
21
|
+
3. (optional) the funnel, the **acquisition** half: the funnel screens a guest sees. Same list → pick → apply shape as automations:
|
|
17
22
|
- `listFunnelTemplates` with the `campaignId`: the template catalog with per-campaign `eligible`/`ineligibleReason`, a `recommended` id, and each template's guest `journey`. Read this before applying; never guess a template id.
|
|
18
23
|
- Pick by what the guest should experience, not by whether the offer has a price:
|
|
19
24
|
- `offer-basic`: Sign Up goes straight to the offer wallet. **No payment step.** The template for any offer redeemed in person, priced or not.
|
|
@@ -22,16 +27,16 @@ Fully doable through the tools. The server does the heavy lifting (id generation
|
|
|
22
27
|
- `applyFunnelTemplate` with `{ "campaignId": ..., "templateId": ... }`. Requires a fresh campaign whose funnel is unset; resolves the referrer from the campaign.
|
|
23
28
|
- The promotion's `canPrePay` flag does **not** change what a template builds; it only gates eligibility. A "no prepay" request means `offer-basic` (or another no-payment template), full stop.
|
|
24
29
|
- After applying, confirm with `listFunnelScreens` that the journey matches intent. For a no-prepay offer there must be no `payment` screen.
|
|
25
|
-
|
|
30
|
+
4. The automations, the **retention** half: the follow-up messaging. **Required whenever the funnel has a sign up form or a checkout**, which covers every `offer-*` and `reservation-offer-*` template. Read `automations.md` before this step. `applyAutomationTemplate` 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`, and check the template's texts against what the offer promises (an expiring-offer template contradicts a "no expiration" offer).
|
|
26
31
|
|
|
27
|
-
Steps
|
|
32
|
+
Steps 3 and 4 are the two halves of a working campaign: the funnel (what the guest sees) and the automations (what happens after they sign up). They are not independent. Outside checkout, the guest's reward is granted by an `awardReward` action inside a sign up automation, so a funnel with no automations signs guests up, hands them a pass with nothing on it, and sends no text. A campaign is not finished until both halves are in place, even when the user only asked about the ad or the landing page. If you stop before the automations, say so plainly in your summary as an open item that blocks going live.
|
|
28
33
|
|
|
29
34
|
### Before a campaign goes live
|
|
30
35
|
|
|
31
36
|
Setting `isPublished: true` with `updateCampaign` puts the campaign in front of guests, and so does switching on its Meta ads. The server checks nothing on either path. So before either one:
|
|
32
37
|
|
|
33
38
|
1. Run `getTaskboard` with `{ "scope": { "type": "campaign", "campaign": { "campaignId": "..." } } }`.
|
|
34
|
-
2. Any `issue` entry with severity `error` blocks going live. The one that matters most is `campaign-automations-missing`: guests would sign up and get nothing. Fix it (step
|
|
39
|
+
2. Any `issue` entry with severity `error` blocks going live. The one that matters most is `campaign-automations-missing`: guests would sign up and get nothing. Fix it (step 4), or stop and tell the user exactly what is missing and what guests would experience. Do not publish around it.
|
|
35
40
|
3. Tell the user about `warning` entries before going live; they can choose to proceed.
|
|
36
41
|
|
|
37
42
|
A short approval like "save it" or "looks good" is not a go-live instruction when the readiness check has not passed. Report what is missing first.
|
|
@@ -48,15 +48,15 @@ A window's `block` is one of two shapes: `once`, with a `utcStart` and `utcEnd`;
|
|
|
48
48
|
`createCreativeStrategy` has two paths behind one tool, and only one of them finishes synchronously:
|
|
49
49
|
|
|
50
50
|
- **`awareness`**: assembled from a fixed template and saved before the call returns. `generationStatus` comes back `complete`.
|
|
51
|
-
- **`cta`**: handed to a background LLM.
|
|
51
|
+
- **`cta`**: handed to a background LLM. Pass `"wait": true` and the call holds up to 20 seconds; if `generationStatus` still reads `"generating"`, **call `getCreativeStrategy` with `"wait": true` until it reads `complete` or `failed`** before using the brief or quoting anything from it. The `jobId` and `jobType` that come back track the same run through `getJob`; reach for that only when the strategy reads `failed` and you want the job's `errorMessage`.
|
|
52
52
|
|
|
53
|
-
`updateCreativeStrategy` is the revision step. Two things to get right: omitting `strategyId` **creates a new strategy** instead of editing the one you meant, and
|
|
53
|
+
`updateCreativeStrategy` is the revision step. Two things to get right: omitting `strategyId` **creates a new strategy** instead of editing the one you meant, and a field you send replaces the stored one whole (fields you leave out keep their stored values), so read first, apply your edits to the full `concepts` array, and send the whole thing back. Generating into a strategy that isn't a draft is rejected rather than silently overwritten.
|
|
54
54
|
|
|
55
55
|
### Recruitment creatives and the recruitment ad
|
|
56
56
|
|
|
57
57
|
The ads that bring applicants in are tool-drivable end to end:
|
|
58
58
|
|
|
59
|
-
1. `createRecruitmentCreatives` with `{ "locationId": "...", "foodCredit": ..., "campaignId": "..." }`. `locationId` and `foodCredit` are required
|
|
59
|
+
1. `createRecruitmentCreatives` with `{ "locationId": "...", "foodCredit": ..., "campaignId": "..." }`. `locationId` and `foodCredit` are required. `foodCredit` is in dollars (25 means $25): `getInfluencerBoardConfig`'s `foodCreditAmountCents` divided by 100. Pass `campaignId` and the tool resolves (or creates) the campaign's recruitment offer itself, which is what groups the creatives and carries the monthly sourcing cap; pass `offerId` instead only when you already have the exact offer. One of the two is needed, or the creatives are generated, charged for, and attached to nothing. Each run calls an image model per missing type; `force` deletes and regenerates the whole set, so don't pass it casually.
|
|
60
60
|
2. `listCreatives`: each creative's `imageKey` is the reference `planAds` takes as a `libraryAsset` (`selectedImageUrl` is the same picked render as a URL). `imageUrl` is the base render, not the ad asset, so don't choose among the image fields yourself. `staleCreativeIds` flags creatives generated from an older version of their offer, and is only populated when you pass `offerId`.
|
|
61
61
|
3. Publish through the `recruitment` template in `ads.md`, declaring the **`linkRecruitmentOffer` effect**; the publish is refused without it. The effect stamps the creatives, links the offer (which the sourcing cap and dashboard spend read), and texts the program's approver that sourcing is live.
|
|
62
62
|
4. Copy rules for the ad live in `ad-copy-creator.md` (`recruitmentAdCopy`; conflating it with guest copy is the classic failure).
|
|
@@ -15,7 +15,7 @@ The retention counterpart to campaigns: flows with no `campaignId`.
|
|
|
15
15
|
`awardReward` grants a reward to a single member right now, like the dashboard's Give Reward button. It is **not** `createMembersProgramReward`: that defines a reward the program offers, this puts one into a specific guest's wallet pass.
|
|
16
16
|
|
|
17
17
|
- Input: `{ "serialNumber": "...", "itemId": "...", "expiresInDays": 14 }`. Get `serialNumber` from `searchUsers` and `itemId` from `listMembersProgramRewards` (or a catalog query). Both are checked against the organization, and a wrong id is rejected rather than granted.
|
|
18
|
-
- Expiry is optional, and a reward with none never expires. `expiresInDays` ends at the end of that day in the restaurant's timezone (what a guest reads "14 days" to mean)
|
|
18
|
+
- Expiry is optional, and a reward with none never expires. `expiresInDays` ends at the end of that day in the restaurant's timezone (what a guest reads "14 days" to mean). `locationId` restricts redemption to one participating location.
|
|
19
19
|
- It recomputes the member's progress, which **re-evaluates their automations**, so a flow triggered by earning a reward will fire (and may text them).
|
|
20
20
|
- **No undo and no idempotency key: a retried call grants a second reward.** Confirm the member, item and expiry with the user before calling, call once per member, and if a call's outcome is unclear, check the member's `rewardAwarded` events with `getMemberConversation` before retrying.
|
|
21
21
|
|
|
@@ -11,11 +11,11 @@
|
|
|
11
11
|
|
|
12
12
|
## The onboarding form
|
|
13
13
|
|
|
14
|
-
Some tasks read self-reported answers rather than observed data: launch date, funnel direction, per-step `isComplete` markers. `getOnboardingForm` reads them (`null` when the org has no form yet); `updateOnboardingForm` writes them (and creates the form when there is none). Top-level keys you omit are left alone. **Nested step objects are replaced, not merged**: read first and send back the whole step you're editing
|
|
14
|
+
Some tasks read self-reported answers rather than observed data: launch date, funnel direction, per-step `isComplete` markers. `getOnboardingForm` reads them (`null` when the org has no form yet); `updateOnboardingForm` writes them (and creates the form when there is none). Top-level keys you omit are left alone. **Nested step objects are replaced, not merged**: read first and send back the whole step you're editing. Setting `pos.details.type` to `"other"` provisions a manual-entry POS location as a side effect.
|
|
15
15
|
|
|
16
16
|
Two POS setup tasks complete off `updateOrganization` instead: `staffInstructions.scan` completes *Members Program Visits POS setup*, and `.prepaid` is additionally required for *Campaign POS setup* when the promotion allows pre-pay. `staffInstructions` is replaced wholesale, so send every key you want to keep.
|
|
17
17
|
|
|
18
|
-
Other `updateOrganization` fields: `timezone` is an IANA zone (`America/New_York`); `minimumSpendValue` is in dollars. `periodCalendar` sets how Impact and revenue plans group weeks into periods: `{"type":"fiscal","fiscal":{"pattern":"4-4-5","yearEndWeekday":"sunday","yearEndRule":"nearestDec31"}}`, where `pattern` is `4-4-5`, `4-5-4`, `5-4-4` or `13x4`, `yearEndWeekday` is `sunday` through `saturday`, and `yearEndRule` is `nearestDec31` or `lastInDecember`. `null` resets it to the default: 13 four-week periods ending on the Sunday nearest Dec 31.
|
|
18
|
+
Other `updateOrganization` fields: `timezone` is an IANA zone (`America/New_York`); `minimumSpendValue` is in dollars. `periodCalendar` sets how Impact and revenue plans group weeks into periods: `{"type":"fiscal","fiscal":{"pattern":"4-4-5","yearEndWeekday":"sunday","yearEndRule":"nearestDec31"}}`, where `pattern` is `4-4-5`, `4-5-4`, `5-4-4` or `13x4`, `yearEndWeekday` is `sunday` through `saturday`, and `yearEndRule` is `nearestDec31` or `lastInDecember`. `null` resets it to the default: 13 four-week periods ending on the Sunday nearest Dec 31.
|
|
19
19
|
|
|
20
20
|
## Establishing brand identity
|
|
21
21
|
|
|
@@ -29,5 +29,5 @@ Browser-only: the brand *import* intelligence (auto-extracting a usable palette
|
|
|
29
29
|
|
|
30
30
|
- **Texting number**: there is no tool to search for or buy one. The restaurant's texting number is bought automatically from its location when its Google place is set (see `updateBrandIdentity` above), or the client chooses one in the dashboard's "Choose texting number" task. If neither has happened, hand the user that task's `completionUrl`.
|
|
31
31
|
- **Media**: `getMediaUploadUrl` (PUT the bytes to the presigned URL, then reference the returned key), `listMedia`, `deleteMedia`. This is how logos and offer images get in through the tools.
|
|
32
|
-
- **Team**: `inviteUser` sends a real email immediately and
|
|
32
|
+
- **Team**: `inviteUser` sends a real email immediately and requires a `role`. OWNER has full billing access; VIEWER is read-only, SCANNER is for staff running the scanner app. A new person gets an invitation valid for 14 days; someone with a Feast account gets a login reminder and is added right away. Re-inviting an email cancels its pending invites and sends a fresh one. Only an OWNER can invite.
|
|
33
33
|
- **Billing**: `getBillingStatus`, read-only: `hasAccess` answers "can they use the product," `needsPayment` flags the states worth acting on and is what the dashboard reads to put the app behind a payment form. `currentTier` and `subscriptionStatus` describe the plan. `existingOrganizations` lists every organization billed under the same billing admin's subscription (this one included when it is on that subscription), with names and tiers; it is absent for per-organization billing. Every billing write stays in the dashboard.
|
|
@@ -11,4 +11,4 @@ One video is one `projectId`, from Generate to the finished MP4. The loop:
|
|
|
11
11
|
|
|
12
12
|
## Reference scripts
|
|
13
13
|
|
|
14
|
-
`
|
|
14
|
+
For a campaign whose brief is not an awareness brief, the `concept` options from `getVideoPromptOptions` are the reference ad scripts Content Studio offers as Concept presets, each distilled from an ad that performed: `label` (its title), `text` (the exact concept Content Studio sends to Bevyl), `description` (what the video shows) and `videoUrl` (a public MP4 preview). The list is the same for every organization. Copy the structure and pacing, not the words. Use one to explain a concept option or to write a concept of your own for `prompt`. An awareness campaign's concepts come from its creative brief instead and carry no video.
|