@feastalytics/cli 0.1.16 → 0.1.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/README.md +12 -3
  2. package/dist/cli.js +170 -232
  3. package/package.json +6 -3
  4. package/skills/feast/SKILL.md +131 -0
  5. package/{feast → skills/feast}/references/domains.md +9 -7
  6. package/{feast → skills/feast}/references/links.md +11 -11
  7. package/skills/feast/references/setup.md +80 -0
  8. package/skills/feast/references/workflows/ad-copy-creator.md +92 -0
  9. package/skills/feast/references/workflows/ad-copy-guest.md +128 -0
  10. package/skills/feast/references/workflows/ads.md +62 -0
  11. package/skills/feast/references/workflows/automations.md +100 -0
  12. package/skills/feast/references/workflows/campaigns.md +75 -0
  13. package/skills/feast/references/workflows/creators.md +96 -0
  14. package/skills/feast/references/workflows/funnels.md +30 -0
  15. package/skills/feast/references/workflows/guests.md +38 -0
  16. package/skills/feast/references/workflows/members-program.md +30 -0
  17. package/skills/feast/references/workflows/onboarding.md +33 -0
  18. package/feast/SKILL.md +0 -105
  19. package/feast/references/setup.md +0 -70
  20. package/feast/references/workflows/ad-copy-creator.md +0 -94
  21. package/feast/references/workflows/ad-copy-guest.md +0 -130
  22. package/feast/references/workflows/ads.md +0 -50
  23. package/feast/references/workflows/automations.md +0 -98
  24. package/feast/references/workflows/campaigns.md +0 -43
  25. package/feast/references/workflows/creators.md +0 -90
  26. package/feast/references/workflows/funnels.md +0 -34
  27. package/feast/references/workflows/guests.md +0 -22
  28. package/feast/references/workflows/members-program.md +0 -26
  29. package/feast/references/workflows/onboarding.md +0 -34
@@ -1,90 +0,0 @@
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
- ### Recruitment creatives and the recruitment ad
54
-
55
- The ads that bring applicants in are CLI-drivable end to end:
56
-
57
- 1. `createRecruitmentCreatives` with the `campaignId` — it resolves (or creates) the campaign's recruitment offer itself, which is what groups the creatives and carries the monthly sourcing cap. Each run calls an image model per missing type; `force` deletes and regenerates the whole set, so don't pass it casually.
58
- 2. `listCreatives` — each creative's `imageKey` is the reference `planAds` takes as a `libraryAsset`; `staleCreativeIds` flags creatives generated from an older version of their offer.
59
- 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.
60
- 4. Copy rules for the ad live in `ad-copy-creator.md` (`recruitmentAdCopy`; conflating it with guest copy is the classic failure).
61
-
62
- ### The decision loop
63
-
64
- 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.
65
- 2. `updateCreatorVisit` with `{ "eventId": "...", "status": "approved" | "denied" }`. **This texts the creator immediately** — approved sends their booking link and creative brief, denied sends a decline and is not reversible from here. 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. Approving a row that is no longer actionable is a no-op and comes back with `changed: false` rather than texting twice.
66
-
67
- The same tool is how you reschedule and how you record what happened. `startTime` moves the visit and texts the creator the new time; `status` also accepts `pending_approval`, `confirmed`, `visited`, `missed`, `issue` and `cancelled`. Pass `sideEffects: false` to write the fields silently — no creator text, no allowance spend, no post-approval automation — which is what you want when you are correcting a record after the fact rather than making the decision now.
68
- 3. The creator books, visits, and submits content on their own — none of that is driven from here.
69
- 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.
70
- 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.
71
-
72
- ### Paying the bonus
73
-
74
- `createInfluencerPayout` charges the organization's card and starts the creator's bonus on its way. **Never call it on your own initiative** — every call needs the client's explicit, fresh approval to pay this specific creator; a standing instruction doesn't count. The endpoint enforces its own preconditions (an ad-approved submission, no payout already active for the visit — one per visit), the amount comes from the board config grossed up to cover the Stripe fee, and after the charge Stripe webhooks carry it to the creator with no further action from you. Follow progress in `queryData` `creators.creatorPayout`, joined to the visit on `visitEventId`.
75
-
76
- ### Conversations
77
-
78
- `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.
79
-
80
- `getCreatorConversation` with a row's `userId` loads the full thread behind it, newest first. Read it before characterizing an exchange or drafting a reply for the human — the queue's last-message snippet is not enough context to speak for a whole conversation.
81
-
82
- **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, read the thread, propose the reply if asked, then hand the user the conversation to send it.
83
-
84
- ### Everything else: queryData
85
-
86
- The `creators` schema exposes `creator` (the person, one row shared across all their applications), `creatorVisitApplication` (one application/visit), and `creatorPayout` (one initiated bonus payout, joined to the visit on `visitEventId`). Join person to visit on `creator.influencerId = creatorVisitApplication.userId`. Use it for anything the tools above don't answer — no-shows, per-location counts, repeat creators, payout history. Content submissions are **not** in the catalog; `listCreatorSubmissions` is the only read.
87
-
88
- > **Not exposed:** replying to a creator's texts or marking a conversation read (the dashboard owns creator messaging), the dashboard's launch-program button itself (its bookkeeping rides the recruitment publish effect — see above), and publishing a creator's submitted content as a partnership ad.
89
-
90
- ---
@@ -1,34 +0,0 @@
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
- **Individual funnel screens are edited** 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` and **reuse it for the rest of the conversation**. One draft holds as many edits as you need, so don't open a second one per change. Start a fresh draft after a promote (which seals the old one) or when you're abandoning what you staged — `discardFunnelDraft` the one you're leaving. Don't adopt a draft you didn't create here: an open one may hold edits someone else staged, and promoting it would ship them. If the user asks you to pick up earlier work, `listFunnelDrafts` shows what's open — confirm which one with them before staging onto it. 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** — two ways, use whichever fits how you can look at things:
19
- - `previewFunnelDraft` renders every screen to a **PDF** (one screen per page, mobile viewport) and returns a short-lived download URL — the option that works when you can read files but not browse.
20
- - The live preview page `https://{referrer}.feastalytics.com/preview/{draftId}/{campaignId}` (drop `/{campaignId}` for a members-program draft) renders the funnel as a flow diagram with the edits applied — the link to hand the user.
21
- Iterate: re-run `listFunnelScreens` **with the `draftId`** to read the funnel *with* the staged edits, stage more, re-preview — until it's right.
22
- 5. **`saveFunnelEdits`** `{ "draftId": "..." }` — **promotes to prod, with no confirmation prompt**: applies the draft's edits to the live funnel and marks the draft `promoted`. To abandon instead, `discardFunnelDraft`.
23
-
24
- **Promote once per piece of work, not once per change.** `stageFunnelEdit` rejects a draft that is not `open`, so a promote ends that draft and the next edit needs a new one. A user asking for four tweaks in a row wants four tweaks, not four promotes — stage them together and promote when the funnel is where they asked for it, or when they say to publish.
25
-
26
- `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).
27
-
28
- ### Domain rules
29
- - **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.
30
- - **Read before every `update`.** Construct the edit from what `listFunnelScreens` returned (renderable ids are stable), never from memory.
31
- - **One edit per `stageFunnelEdit`**, staged incrementally; each is validated as it lands.
32
- - **Drift guard.** `saveFunnelEdits` rejects the promote if the live funnel changed since the draft was created — re-create the draft in that case. Do not lean on it to catch someone else editing the same funnel; keeping to one open draft (step 2) is the real protection.
33
-
34
- ---
@@ -1,22 +0,0 @@
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
- `searchUsers` 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
- `getMemberConversation` with a member's `serialNumber` loads their thread, newest first — the pair to `searchUsers` the same way `getCreatorConversation` pairs with `listCreatorConversations`. Always pass `eventTypes`: `["sentText","receivedText"]` is the SMS thread, and adding `scan`/`order`/`checkout`/`rewardAwarded`/`rewardRedeemed` interleaves what happened between the messages. Unfiltered it returns the member's entire history unpaginated.
11
-
12
- **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.
13
-
14
- ## Everything else: the data catalog
15
-
16
- `searchUsers` answers "recent activity, one event per member." Every other read question about guests — and about orders, menu items, texts, reservations, creator visits, payouts — goes through **`describeData` → `queryData`**:
17
-
18
- - `describeData` with no arguments returns the index of every queryable object type plus the full query grammar; narrowed by schema or object type it returns full column detail. Never guess column names.
19
- - `queryData` is read-only and always scoped to the calling organization — never filter on organizationId yourself.
20
- - Six schemas: `interface` (POS-agnostic orders, order items, menu `catalogItem`s, `location`s, reservations — same shape whichever POS the org runs), `core` (guests/members), `events` (user events), `texting` (SMS logs), `creators` (visits and payouts), `attribution` (campaign attribution). Prefer `interface` for anything POS-shaped.
21
-
22
- Typical uses: visit counts and cohorts, order history for one guest, menu items with real prices for grounding copy, text delivery history, creator payout status.
@@ -1,26 +0,0 @@
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 fully manageable.** `listMembersProgramRewards` returns every reward with its catalog item's `name`, `staffInstructions`, `pointsCost` and `source` — the item is resolved across the Feast, Toast, Square and Clover catalogs, and a `null` name means it no longer exists in any of them.
12
- - **Creating takes one of two shapes.** `{ "type": "item", "itemId" }` promotes an existing catalog item — prefer it whenever the item already exists in the POS. `{ "type": "name", "name" }` looks the name up across all four catalogs and creates a new Feast item only if nothing matches; **the match is exact, so a near-miss silently duplicates a menu item the restaurant already has** — check `listMembersProgramRewards` or the catalog first. A name shared by several items returns a CONFLICT listing candidates so you can pass `itemId` instead. `staffInstructions` only exist on Feast items and are rejected for POS-sourced ones.
13
- - **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. To find orphans, cross-reference `listAutomations` for `awardReward` actions carrying the reward's `itemId`.
14
- - `updateMembersProgramReward` corrects a reward in place — `pointsCost: null` converts a points reward into an automation-granted one. `deleteMembersProgramReward` is the orphan cleanup; it leaves the catalog item alone (it may be a real menu item) and doesn't claw back anything already redeemed.
15
-
16
- ---
17
-
18
- ## Wallet pass configuration
19
-
20
- The pass (the wallet membership card) is read and written as a whole document.
21
-
22
- - **`getPassConfiguration`** `{}` — returns the latest live configuration: `sections`, `features`, `locations`, `metadata`, and its `version`.
23
- - **`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 — there is no confirmation prompt, so treat it with the same care as a live send.
24
- - Pass **image generation** (punch-card strips etc.) is not exposed — image workflows still need the app.
25
-
26
- ---
@@ -1,34 +0,0 @@
1
- # Onboarding and brand
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
- ## Onboarding tasks (the taskboard)
7
-
8
- `getTaskboard` is the single "what needs fixing or finishing" surface: one `entries` list discriminated by `kind`. `task` entries are the org's onboarding tasks; `issue` entries are live-computed misconfigurations with a `fixHint`. Scope with `{"scope":{"type":"onboarding"}}` for tasks only, `{"type":"task","task":{"taskId":"..."}}` for one task, or leave the default `all`.
9
-
10
- - **Start from `completionInstructions`, not guesswork.** Every task entry says exactly what completes it and whether it needs a human in a browser. Trust it over inferring from the task name.
11
- - **Split the work accordingly.** Campaigns, automations, funnel fixes, rewards, brand identity, the phone number, image uploads and the onboarding form are all completable through CLI tools — do them. Tasks that need OAuth (Facebook, POS), physical device setup, or in-restaurant staff training cannot be: hand the user that task's **`completionUrl`** — a page where they complete exactly that task. Paste the URL directly in your reply; in the dashboard chat it opens the task next to the conversation, and in a terminal it's clickable.
12
- - **Never claim a task complete or try to mark one.** Statuses are derived from live data by a recompute (triggered by every taskboard read, ~30s lag) — do the underlying work, then re-read the taskboard to confirm the checkmark flipped.
13
- - Working through onboarding = repeat: `getTaskboard` (scope `onboarding`) → do the CLI-doable incomplete required tasks → hand over completionUrls for the rest → re-read to verify.
14
-
15
- ## The onboarding form
16
-
17
- 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. **Nested step objects are replaced, not merged** — read first and send back the whole step you're editing (`data` is the exception and is merged). Setting `pos.details.type` to `"other"` provisions a manual-entry POS location as a side effect.
18
-
19
- 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 — send every key you want to keep.
20
-
21
- ## Establishing brand identity
22
-
23
- 1. `searchGooglePlaces` — resolve the restaurant to its Google Place. Include the city in the query; only trust `confidentMatch` when it's non-null, otherwise show the candidates and let the customer pick.
24
- 2. `createBrandIdentity` — the branded site: a subdomain, layout config, and a full default screen tree. The subdomain is claimed **across all organizations** and gates everything funnel-shaped downstream, so confirm the name with the customer first. Get `logoUrl` via `getMediaUploadUrl`.
25
- 3. `updateBrandIdentity` — business data, theme, tracking pixel IDs, OpenTable links, and the Google Place link. It deep-merges, so send only what you're changing. **Setting `googleConfig.placeId` enqueues a review/photo scrape; changing an existing placeId orphans everything scraped under the old one** — confirm before replacing.
26
-
27
- Browser-only: the brand *import* intelligence — auto-extracting a usable palette and logo from a scraped site lives in the app, not the API. If the customer wants that flow, hand them the dashboard.
28
-
29
- ## Plumbing the taskboard leans on
30
-
31
- - **Phone number** — `searchAvailablePhoneNumbers` (free, search by the restaurant's own postal code or coordinates — proximity beats a memorable area code) then `purchaseAndConfigurePhoneNumber`, which **bills the account irreversibly**. Establish where the restaurant actually is before buying.
32
- - **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 from the CLI.
33
- - **Team** — `inviteUser` sends a real email immediately and **defaults to OWNER** (full billing access) — always pass `role` explicitly; VIEWER is read-only, SCANNER is for staff running the scanner app.
34
- - **Billing** — `getBillingStatus`, read-only: `hasAccess` answers "can they use the product," `needsPayment` flags the states worth acting on. Every billing write stays in the dashboard.