@feastalytics/cli 0.1.18 → 0.1.19

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/README.md CHANGED
@@ -10,7 +10,7 @@ Feastalytics ships as a plugin (the `feast` skill plus the hosted MCP server at
10
10
 
11
11
  - **Claude Code**: `/plugin marketplace add feastalytics/cli`, then `/plugin install feastalytics@feast`.
12
12
  - **claude.ai and Claude Desktop**: add a custom connector with the URL `https://mcp.feast-api.com/mcp`. A directory listing is to follow.
13
- - **ChatGPT**: `npm run build:chatgpt` writes `dist/feastalytics-chatgpt.zip` (the root `plugin.json`, `mcp.json` and `skills/`).
13
+ - **ChatGPT**: `npm run build:chatgpt` writes `dist/feastalytics-chatgpt.zip` (`plugin/plugin.json`, `plugin/mcp.json`, the icon and `plugin/skills/`).
14
14
  - **Skill only** (Claude Code, Codex, Cursor and other agents): `npx skills add feastalytics/cli`.
15
15
 
16
16
  ### CLI
@@ -75,7 +75,7 @@ Mutations additionally require `--org` and print the server-resolved org before
75
75
 
76
76
  ## Agent skill
77
77
 
78
- The `skills/feast/` directory is an [agent skill](https://www.skills.sh) that teaches an agent to operate Feastalytics through its tools, either this CLI or the hosted MCP server at `https://mcp.feast-api.com/mcp` (same tools, same names). Install it into your agent(s):
78
+ The `plugin/skills/feast/` directory is an [agent skill](https://www.skills.sh) that teaches an agent to operate Feastalytics through its tools, either this CLI or the hosted MCP server at `https://mcp.feast-api.com/mcp` (same tools, same names). Install it into your agent(s):
79
79
 
80
80
  ```bash
81
81
  npx skills add feastalytics/cli
package/dist/cli.js CHANGED
@@ -9045,6 +9045,35 @@ var CLI_MANIFEST = {
9045
9045
  "$schema": "http://json-schema.org/draft-07/schema#"
9046
9046
  }
9047
9047
  },
9048
+ {
9049
+ "id": "approveVideo",
9050
+ "domain": "ads",
9051
+ "description": "Approve a video and save its MP4 to the campaign's creative library, returning the libraryKey. Pass the projectId; runId is optional and defaults to the latest run that has an export (the one getVideo's exportUrl plays), so pass it only to approve an earlier run from listCampaignVideos. Fails until that run's export is ready (getVideo pipeline.status ready). Only once the human has watched the video and approved it.",
9052
+ "type": "mutation",
9053
+ "path": [
9054
+ "api",
9055
+ "bevyl",
9056
+ "approveProject"
9057
+ ],
9058
+ "inputJsonSchema": {
9059
+ "type": "object",
9060
+ "properties": {
9061
+ "projectId": {
9062
+ "type": "string",
9063
+ "minLength": 1
9064
+ },
9065
+ "runId": {
9066
+ "type": "string",
9067
+ "minLength": 1
9068
+ }
9069
+ },
9070
+ "required": [
9071
+ "projectId"
9072
+ ],
9073
+ "additionalProperties": false,
9074
+ "$schema": "http://json-schema.org/draft-07/schema#"
9075
+ }
9076
+ },
9048
9077
  {
9049
9078
  "id": "awardReward",
9050
9079
  "domain": "membersProgram",
@@ -13530,7 +13559,7 @@ var CLI_MANIFEST = {
13530
13559
  {
13531
13560
  "id": "createRecruitmentCreatives",
13532
13561
  "domain": "creators",
13533
- "description": "Generate the five AI recruitment ad creatives for a location. Pass campaignId (or offerId) so they attach to its recruitment offer. Only missing types are generated; force deletes and regenerates the whole set. foodCredit from getInfluencerBoardConfig.",
13562
+ "description": "Render the five recruitment ad images for a location from fixed templates, using its own photo, logo and food credit (no AI image generation). Pass campaignId (or offerId) so they attach to its recruitment offer. Only missing types are generated; force deletes and regenerates the whole set. foodCredit from getInfluencerBoardConfig.",
13534
13563
  "type": "mutation",
13535
13564
  "path": [
13536
13565
  "api",
@@ -13843,6 +13872,90 @@ var CLI_MANIFEST = {
13843
13872
  "$schema": "http://json-schema.org/draft-07/schema#"
13844
13873
  }
13845
13874
  },
13875
+ {
13876
+ "id": "generateVideo",
13877
+ "domain": "ads",
13878
+ "description": "Start a Bevyl ad video for a campaign from its b-roll. Returns { projectId, pipelineStatus } at once; the server uploads the clips, waits for Bevyl to process them, creates the video and exports it. Poll getVideo with the projectId every 30 seconds or so until pipeline.status is ready (exportUrl is the MP4) or failed (message says why). Spends Bevyl generation credits, so only with the human's explicit approval. Build prompt from getVideoPromptOptions (its defaultPrompt, edited or not): Bevyl reads it verbatim, up to 5000 characters. Set format yourself (talking-head, voiceover, trending-sounds which needs trendId, or no-audio); the chosen direction's suggestedFormat is a good default. Optional: angleId from listCampaignVideos to add a version to an existing angle (otherwise angleTitle names a new one, defaulting to the concept's first line), voiceoverProfileId, backgroundMusicTrackId, durationSeconds 10 to 80 in steps of 5, and brollKeys (S3 keys from listMedia scope creativeLibraryBroll; omit to use every clip already synced to Bevyl).",
13879
+ "type": "mutation",
13880
+ "path": [
13881
+ "api",
13882
+ "bevyl",
13883
+ "generate"
13884
+ ],
13885
+ "inputJsonSchema": {
13886
+ "type": "object",
13887
+ "properties": {
13888
+ "campaignId": {
13889
+ "type": "string",
13890
+ "minLength": 1
13891
+ },
13892
+ "angleId": {
13893
+ "type": "string",
13894
+ "minLength": 1,
13895
+ "maxLength": 100
13896
+ },
13897
+ "angleTitle": {
13898
+ "type": "string",
13899
+ "minLength": 1,
13900
+ "maxLength": 300
13901
+ },
13902
+ "prompt": {
13903
+ "type": "string",
13904
+ "minLength": 1,
13905
+ "maxLength": 5e3
13906
+ },
13907
+ "format": {
13908
+ "type": "string",
13909
+ "enum": [
13910
+ "talking-head",
13911
+ "voiceover",
13912
+ "trending-sounds",
13913
+ "no-audio"
13914
+ ],
13915
+ "default": "talking-head"
13916
+ },
13917
+ "voiceoverProfileId": {
13918
+ "type": "string",
13919
+ "minLength": 1
13920
+ },
13921
+ "trendId": {
13922
+ "type": "string",
13923
+ "minLength": 1
13924
+ },
13925
+ "backgroundMusicTrackId": {
13926
+ "type": "string",
13927
+ "minLength": 1
13928
+ },
13929
+ "durationSeconds": {
13930
+ "type": "integer",
13931
+ "minimum": 10,
13932
+ "maximum": 80,
13933
+ "multipleOf": 5
13934
+ },
13935
+ "brollKeys": {
13936
+ "type": "array",
13937
+ "items": {
13938
+ "type": "string",
13939
+ "minLength": 1
13940
+ },
13941
+ "maxItems": 100
13942
+ },
13943
+ "bevylVideoIds": {
13944
+ "type": "array",
13945
+ "items": {
13946
+ "type": "string",
13947
+ "minLength": 1
13948
+ },
13949
+ "maxItems": 100
13950
+ }
13951
+ },
13952
+ "required": [
13953
+ "campaignId"
13954
+ ],
13955
+ "additionalProperties": false,
13956
+ "$schema": "http://json-schema.org/draft-07/schema#"
13957
+ }
13958
+ },
13846
13959
  {
13847
13960
  "id": "getAutomationDraft",
13848
13961
  "domain": "automations",
@@ -14805,6 +14918,56 @@ var CLI_MANIFEST = {
14805
14918
  "$schema": "http://json-schema.org/draft-07/schema#"
14806
14919
  }
14807
14920
  },
14921
+ {
14922
+ "id": "getVideo",
14923
+ "domain": "ads",
14924
+ "description": "Read one Content Studio video by projectId. pipeline.status moves uploading, processing, creating, rendering, exporting, then ready (exportUrl is the MP4 to watch) or failed (message says why). pipeline is null for videos made before the server pipeline; use listCampaignVideos for those. Reads Feastalytics only, so it is cheap to poll.",
14925
+ "type": "query",
14926
+ "path": [
14927
+ "api",
14928
+ "bevyl",
14929
+ "getProject"
14930
+ ],
14931
+ "inputJsonSchema": {
14932
+ "type": "object",
14933
+ "properties": {
14934
+ "projectId": {
14935
+ "type": "string",
14936
+ "minLength": 1
14937
+ }
14938
+ },
14939
+ "required": [
14940
+ "projectId"
14941
+ ],
14942
+ "additionalProperties": false,
14943
+ "$schema": "http://json-schema.org/draft-07/schema#"
14944
+ }
14945
+ },
14946
+ {
14947
+ "id": "getVideoPromptOptions",
14948
+ "domain": "ads",
14949
+ "description": "The building blocks for a generateVideo prompt for one campaign. defaultPrompt is what Content Studio would send with its default picks: plain text in four sections (Campaign, Concept, Creative direction, CTA), each a heading line followed by its text. campaign, concept, direction and cta list the alternatives for each section ({ id, label, text }); concept options also carry the reference video they were distilled from, and direction options a suggestedFormat to pass as generateVideo's format. Send defaultPrompt as is, or swap a section's text for another option's, or rewrite it entirely: generateVideo sends the prompt to Bevyl verbatim. Reads Feastalytics only.",
14950
+ "type": "query",
14951
+ "path": [
14952
+ "api",
14953
+ "bevyl",
14954
+ "getVideoPromptOptions"
14955
+ ],
14956
+ "inputJsonSchema": {
14957
+ "type": "object",
14958
+ "properties": {
14959
+ "campaignId": {
14960
+ "type": "string",
14961
+ "minLength": 1
14962
+ }
14963
+ },
14964
+ "required": [
14965
+ "campaignId"
14966
+ ],
14967
+ "additionalProperties": false,
14968
+ "$schema": "http://json-schema.org/draft-07/schema#"
14969
+ }
14970
+ },
14808
14971
  {
14809
14972
  "id": "inviteUser",
14810
14973
  "domain": "core",
@@ -14965,6 +15128,31 @@ var CLI_MANIFEST = {
14965
15128
  ],
14966
15129
  "inputJsonSchema": null
14967
15130
  },
15131
+ {
15132
+ "id": "listCampaignVideos",
15133
+ "domain": "ads",
15134
+ "description": "Every Content Studio video for a campaign, newest first: projectId, angleId and angleTitle (videos are grouped into angles), the concept and direction they were made from, latestRun (the Bevyl run, whose runId approveVideo needs), approvedRunId, edits, and pipeline (status, message, exportUrl) for videos made through generateVideo. Calls Bevyl to read run status, so prefer getVideo to poll a single video.",
15135
+ "type": "query",
15136
+ "path": [
15137
+ "api",
15138
+ "bevyl",
15139
+ "listCampaignProjects"
15140
+ ],
15141
+ "inputJsonSchema": {
15142
+ "type": "object",
15143
+ "properties": {
15144
+ "campaignId": {
15145
+ "type": "string",
15146
+ "minLength": 1
15147
+ }
15148
+ },
15149
+ "required": [
15150
+ "campaignId"
15151
+ ],
15152
+ "additionalProperties": false,
15153
+ "$schema": "http://json-schema.org/draft-07/schema#"
15154
+ }
15155
+ },
14968
15156
  {
14969
15157
  "id": "listCreatives",
14970
15158
  "domain": "creators",
@@ -15710,6 +15898,41 @@ var CLI_MANIFEST = {
15710
15898
  "$schema": "http://json-schema.org/draft-07/schema#"
15711
15899
  }
15712
15900
  },
15901
+ {
15902
+ "id": "requestVideoEdit",
15903
+ "domain": "ads",
15904
+ "description": `Ask Bevyl to remake a video with one change, for example "make the hook punchier" or "use the patio shots first". Send only the change in note (600 characters at most); Bevyl builds from the video's current state. On-screen text cannot be moved or resized. Starts a new run: getVideo moves back to rendering, then ready with the new export. Spends Bevyl generation credits, so only with the human's explicit approval.`,
15905
+ "type": "mutation",
15906
+ "path": [
15907
+ "api",
15908
+ "bevyl",
15909
+ "regenerateProject"
15910
+ ],
15911
+ "inputJsonSchema": {
15912
+ "type": "object",
15913
+ "properties": {
15914
+ "projectId": {
15915
+ "type": "string",
15916
+ "minLength": 1
15917
+ },
15918
+ "note": {
15919
+ "type": "string",
15920
+ "minLength": 1,
15921
+ "maxLength": 600
15922
+ },
15923
+ "requestId": {
15924
+ "type": "string",
15925
+ "format": "uuid"
15926
+ }
15927
+ },
15928
+ "required": [
15929
+ "projectId",
15930
+ "note"
15931
+ ],
15932
+ "additionalProperties": false,
15933
+ "$schema": "http://json-schema.org/draft-07/schema#"
15934
+ }
15935
+ },
15713
15936
  {
15714
15937
  "id": "saveAutomationEdits",
15715
15938
  "domain": "automations",
@@ -22321,23 +22544,6 @@ var CLI_MANIFEST = {
22321
22544
  },
22322
22545
  "additionalProperties": false
22323
22546
  },
22324
- "openTableConfig": {
22325
- "type": "object",
22326
- "properties": {
22327
- "baseUrl": {
22328
- "type": "string"
22329
- },
22330
- "numDaysCanReserveAhead": {
22331
- "type": "number"
22332
- }
22333
- },
22334
- "required": [
22335
- "baseUrl",
22336
- "numDaysCanReserveAhead"
22337
- ],
22338
- "additionalProperties": false,
22339
- "description": "Deprecated: Use openTableConfigs instead"
22340
- },
22341
22547
  "openTableConfigs": {
22342
22548
  "type": "array",
22343
22549
  "items": {
@@ -23603,13 +23809,6 @@ var CLI_MANIFEST = {
23603
23809
  "maxItems": 20,
23604
23810
  "description": "Additional email addresses for scheduled, rescheduled, confirmed and cancelled visit emails and Calendar invitations. No Feast account is required. Send an empty array to remove all email-only recipients; omit to preserve them. Requires the organization's booking notification feature to be enabled."
23605
23811
  },
23606
- "passConfigured": {
23607
- "type": "boolean"
23608
- },
23609
- "calendarConfigured": {
23610
- "type": "boolean",
23611
- "description": "Ignored. Kept while older clients still send it."
23612
- },
23613
23812
  "maxBookingDaysOut": {
23614
23813
  "anyOf": [
23615
23814
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@feastalytics/cli",
3
- "version": "0.1.18",
3
+ "version": "0.1.19",
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": {
@@ -9,7 +9,7 @@
9
9
  "files": [
10
10
  "dist",
11
11
  "!dist/*.zip",
12
- "skills"
12
+ "plugin/skills"
13
13
  ],
14
14
  "author": "Feastalytics",
15
15
  "license": "UNLICENSED",
@@ -30,7 +30,7 @@
30
30
  "feast": "tsx src/cli.ts",
31
31
  "typecheck": "tsc --noEmit",
32
32
  "prepublishOnly": "npm run build",
33
- "version": "node scripts/sync-plugin-version.mjs && git add .claude-plugin/plugin.json plugin.json",
33
+ "version": "node scripts/sync-plugin-version.mjs && git add plugin/.claude-plugin/plugin.json plugin/plugin.json",
34
34
  "build:chatgpt": "sh scripts/build-chatgpt-zip.sh"
35
35
  },
36
36
  "devDependencies": {
@@ -85,6 +85,7 @@ Many tasks are multi-step and have a required ordering the app normally enforces
85
85
  | Writing guest-facing Meta ad copy (`adCopy`) | `references/workflows/ad-copy-guest.md` |
86
86
  | Writing creator-recruitment ad copy (`recruitmentAdCopy`) | `references/workflows/ad-copy-creator.md` |
87
87
  | Publishing, pausing, budgeting or diagnosing Meta ads | `references/workflows/ads.md` |
88
+ | Making a video ad with Bevyl: prompt, generate, edit, approve | `references/workflows/videos.md` |
88
89
  | Creator sourcing: approving applicants, reviewing content, creatives, payouts, reimbursements, texting a creator | `references/workflows/creators.md` |
89
90
  | Members-program rewards, granting a reward to one member; reading or saving the wallet pass configuration | `references/workflows/members-program.md` |
90
91
  | Working the onboarding taskboard; brand identity; phone, media, invites, billing | `references/workflows/onboarding.md` |
@@ -41,6 +41,10 @@ Restaurants recruit local content creators to visit and post. One config per loc
41
41
 
42
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`.
43
43
 
44
+ ## Video ads (Content Studio)
45
+
46
+ Bevyl edits a campaign's b-roll into ad videos, grouped into angles. One `projectId` per video: `getVideoPromptOptions` → `generateVideo` → poll `getVideo` → `requestVideoEdit` or `approveVideo`. See `workflows/videos.md`.
47
+
44
48
  ## The data catalog
45
49
 
46
50
  `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.
@@ -30,7 +30,7 @@ If the global install fails on permissions, don't retry with `sudo`. Tell the us
30
30
 
31
31
  ### Authenticating
32
32
 
33
- Authenticate once. Tokens are cached in `~/.config/feast-cli/credentials.json` and refreshed automatically:
33
+ Authenticate once. The CLI keeps you signed in and refreshes the session itself:
34
34
 
35
35
  ```bash
36
36
  feast login # opens a browser to authorize (default)
@@ -64,7 +64,7 @@ Feastalytics publishes further skills that build on this one (campaign diagnosis
64
64
 
65
65
  ```bash
66
66
  feast skill list # what is published for your organization
67
- feast skill install feast-playbooks # install or refresh one into ~/.claude/skills/
67
+ feast skill install feast-playbooks # install or refresh one for your agent
68
68
  ```
69
69
 
70
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.
@@ -55,8 +55,4 @@ An effect that reports `error` in the job is a case for the dashboard, not for p
55
55
  - `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
56
  - `ads_get_datasets` / `ads_create_dataset`: pixel checks and creation. The pixel a campaign should optimise against is the one its funnel actually fires (from the layout config), not whichever pixel looks plausible on the account. After creating one, write its id back with `updateBrandIdentity`; creation alone connects nothing. That layout config value is what makes the funnel fire the pixel and what the onboarding task reads.
57
57
 
58
- ### Reference scripts for video ads
59
-
60
- `listReferenceScripts` returns the reference ad scripts Content Studio offers as Concept presets for Bevyl videos, each distilled from an ad that performed: `description` (what the video shows), `videoUrl` (a public MP4 preview), `structure` (the ordered beats), `keyPhrases` (lines to adapt, with `<placeholders>` filled from the campaign's facts) and `concept` (the exact text Content Studio sends to Bevyl). Copy the structure and pacing, not the words. The list is the same for every organization.
61
-
62
58
  > **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.
@@ -89,12 +89,14 @@ When an automation's trigger is `receiveAutomation`, ask the user whether it sho
89
89
 
90
90
  ### Text-content best practices (rules when creating, checklist when reviewing)
91
91
 
92
- 1. **Descriptive names**: "Day 2: Visit Reminder with Pass Link", not "Reminder 1".
93
- 2. **Lead with the pass link**: the first post-signup text MUST include it ("add your pass: {{pass link}}").
94
- 3. **Always `https://`** on every link (carriers block bare/protocol-less links).
92
+ 1. **Descriptive names**: "Day 2: Visit Reminder with Wallet Link", not "Reminder 1".
93
+ 2. **Lead with the wallet link**: the first post-signup text MUST include the link that adds the guest's card to Apple or Google Wallet, written with the `passLink` text variable (see the variables below).
94
+ 3. **Every link starts with https**: carriers block links written without it.
95
95
  4. **Mobile Google Maps links only**: `https://maps.app.goo.gl/...`, never desktop `maps.google.com`.
96
96
  5. **Correct reservation links**: `https://{subdomain}.feastalytics.com/i/{shorthand}/reservation` using the *current* campaign's shorthand (from `listCampaigns`) and a valid subdomain. Never reuse another campaign's link.
97
- 6. **Personalize** with `{{firstName}}`; **vary** tone/wording across automations; **re-share** useful info (pass link, hours, maps, reservation) in reminders; keep **empty lines** between blocks for readability.
97
+ 6. **Personalize** with the `firstName` variable; **vary** tone/wording across automations; **re-share** useful info (wallet link, hours, maps, reservation) in reminders; keep **empty lines** between blocks for readability.
98
98
  7. **Align offer expirations with open hours**: never expire an offer while the restaurant is closed.
99
99
 
100
+ **Text variables.** A text action fills these in per guest when it sends. Write each one as its name wrapped in double curly braces (two opening braces, the name, two closing braces), exactly as spelled: `firstName`, `lastName`, `memberNumber`, `serialNumber`, `progress`, `passLink` (the guest's wallet link), `referralLink` (the campaign referral link), `googleMapsLink`, `membersProgramLink`, `visitCount`, `offer`. Any other name is sent to the guest as literal text.
101
+
100
102
  ---
@@ -52,7 +52,7 @@ Three tools, all keyed by the Feast campaign `id` from `listCampaigns` (a UUID),
52
52
 
53
53
  **Units.** `count`; `percent` as 0 to 100 (not 0 to 1); `usd` in dollars (not cents); `days`; `multiple` for ROAS (2 means 2x). In the breakdown: sessions, visitors, signups, impressions and reach are counts; every `*Rate`, `thumbStopRatio`, `holdRate` and `uniqueClickthrough` are percents; spend, cpm, revenue, costPerSignup and revenuePerSignup are USD; averageTimeToShow is days from signup to first scan.
54
54
 
55
- **Headline numbers: `getCampaignKpis`** with `{ "campaignId": "...", "start"?: ..., "end"?: ... }`. Pass both `start` and `end` for a date range; with either missing it covers all time. `"isPrimaryOnly": true` returns only the primary metrics. Returns one `{ id, type, value, unit }` row per metric, covering ad performance (spend, impressions, hook rate, hold rate, CTR, from synced Facebook data, so ROAS is revenue divided by spend), the funnel, automations and results. Rate metrics with a target band also carry `benchmark: { min, good, great }` in the same unit. A metric whose value would be zero is left out rather than returned as 0. So a missing spend row means no spend or no Facebook sync yet, never a confirmed $0.
55
+ **Headline numbers: `getCampaignKpis`** with `{ "campaignId": "...", "start"?: ..., "end"?: ... }`. Send both `start` and `end` for a date range; with either missing it covers all time. `"isPrimaryOnly": true` returns only the primary metrics. Returns one `{ id, type, value, unit }` row per metric, covering ad performance (spend, impressions, hook rate, hold rate, CTR, from synced Facebook data, so ROAS is revenue divided by spend), the funnel, automations and results. Rate metrics with a target band also carry `benchmark: { min, good, great }` in the same unit. A metric whose value would be zero is left out rather than returned as 0. So a missing spend row means no spend or no Facebook sync yet, never a confirmed $0.
56
56
 
57
57
  **Grading: `getCampaignBenchmarks`** (no input) returns `{ id, label, unit, description, formula, benchmark }` for every metric id. Call it once and reuse it; it is the same for every campaign. Grade a value green at or above `good`, yellow at or above `min`, red below `min`; `great` is a stretch level (null for ROAS). `benchmark` is null when a metric has no target band. The bands are fleet percentiles (P25/P50/P75 of campaigns with over 500 visitors), rounded, not per organization; ROAS is anchored at 1x break-even.
58
58
 
@@ -69,7 +69,7 @@ The ads that bring applicants in are tool-drivable end to end:
69
69
  The same tool is how you reschedule and how you record what happened. `startTime` set to a date texts the creator a confirmation and alerts the approver; `null` clears the time and texts the creator asking for a new one. `startTime` is rejected while the row is `pending_approval` and in any call that passes `status: "approved"`, so approve first, then set the time in a second call (`status: "pending_approval"` clears the time itself; don't pass `startTime` with it). `status` also accepts `confirmed`, `visited`, `missed`, `issue` and `cancelled`; of these only `cancelled` texts the creator. `locationId` moves the visit to another location with a creator program and texts no one, so tell the creator yourself. `notes` sets staff notes shown on the scanner, never sent to the creator. Pass `sideEffects: false` to make any update silent (same field writes, but no creator text, no allowance spend, no post-approval automation), which is what you want when correcting a record after the fact rather than making the decision now.
70
70
  3. The creator books, visits, and submits content on their own; none of that is driven from here.
71
71
  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.
72
- 5. `decideCreatorSubmission`: `approved`, `rejected`, `revision_requested`, or `under_review`. **Approving texts the creator too**, unless you pass `skipApprovalText: true` (use that only for silent record corrections). `revision_requested` always texts: it sends your `feedbackMessage` verbatim plus a resubmit link, so write it as something the creator will read, not an internal note. **Always pass `approvalType` explicitly when approving**, because an omitted one means `"ad"`: `"ad"` means the content may run in paid ads, stamps the board's bonus on the submission and marks it pending (paid later through `createInfluencerPayout`), and is rejected when the board's bonus is $0; `"organic"` is for content only on their own channels, and earns no payout. Re-approving an approved submission is rejected, except upgrading an `organic` approval to `ad`.
72
+ 5. `decideCreatorSubmission`: `approved`, `rejected`, `revision_requested`, or `under_review`. **Approving texts the creator too**, unless you send `skipApprovalText: true` (use that only for silent record corrections). `revision_requested` always texts: it sends your `feedbackMessage` verbatim plus a resubmit link, so write it as something the creator will read, not an internal note. **Always send `approvalType` explicitly when approving**, because an omitted one means `"ad"`: `"ad"` means the content may run in paid ads, stamps the board's bonus on the submission and marks it pending (paid later through `createInfluencerPayout`), and is rejected when the board's bonus is zero; `"organic"` is for content only on their own channels, and earns no payout. Re-approving an approved submission is rejected, except upgrading an `organic` approval to `ad`.
73
73
 
74
74
  ### Paying the bonus
75
75
 
@@ -26,7 +26,7 @@ The recipient is always named by id, never by phone number, and the type must ma
26
26
 
27
27
  - `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 (type, enum values, nullability, description, and the link names `pivot` and `join` take). Pass `includeGrammar: false` once you have the grammar. Never guess column names.
28
28
  - `queryData` is read-only and always scoped to the organization; never filter on organizationId yourself.
29
- - Writing a query: `commands` run in order (`filter`, `pivot`, `join`, `aggregate`), and `pivot` and `join` must come before any `aggregate`. A filter leaf is one column, written as the column name prefixed with `$`; combine leaves with `{ "type": "and" | "or", "filters": [...] }`. Use `{ "strings": [...] }` for any-of rather than a large `or`. Pass `args.fields` to return only the columns you need on wide object types, and page by passing the returned `nextCursor` back as `args.cursor` (no `nextCursor` means no more rows).
29
+ - Writing a query: `commands` run in order (`filter`, `pivot`, `join`, `aggregate`), and `pivot` and `join` must come before any `aggregate`. A filter leaf is one column, written as the column name prefixed with `$`; combine leaves with `{ "type": "and" | "or", "filters": [...] }`. Use `{ "strings": [...] }` for any-of rather than a large `or`. Send `args.fields` to return only the columns you need on wide object types, and page by sending the returned `nextCursor` back as `args.cursor` (no `nextCursor` means no more rows).
30
30
  - Example, opted-in members with more than 5 visits, newest first:
31
31
  ```json
32
32
  { "schemaName": "core", "objectTypeName": "guest",
@@ -0,0 +1,14 @@
1
+ # Video ads (Bevyl)
2
+
3
+ ## Making a video
4
+
5
+ One video is one `projectId`, from Generate to the finished MP4. The loop:
6
+
7
+ 1. **`getVideoPromptOptions({ campaignId })`**. `defaultPrompt` is what Content Studio would send: four plain-text sections (Campaign, Concept, Creative direction, CTA), each a heading line then its text. `campaign`, `concept`, `direction` and `cta` list the alternatives for each section (`{ id, label, text }`). Concepts carry the reference video they came from (`videoUrl`, `description`), and directions a `suggestedFormat`. Show the human the prompt you plan to send. Swap a section for another option's text or rewrite it as they ask.
8
+ 2. **`generateVideo`** with `campaignId`, `prompt` (sent to Bevyl verbatim, 5,000 characters at most) and `format` (`voiceover`, `talking-head`, `trending-sounds` with a `trendId`, or `no-audio`; the chosen direction's `suggestedFormat` is the default). Pass `angleId` from `listCampaignVideos` to add a version to an existing angle; otherwise a new angle is named from the Concept's first line, or from `angleTitle`. Footage is every clip already synced to Bevyl unless you pass `brollKeys` (S3 keys from `listMedia` scope `creativeLibraryBroll`). **This spends Bevyl credits: only with the human's explicit go-ahead.**
9
+ 3. **`getVideo({ projectId })`** about every 30 seconds. `pipeline.status` moves uploading, processing, creating, rendering, exporting, then `ready` (`exportUrl` is the MP4) or `failed` (`message` says why). A first video takes several minutes.
10
+ 4. Send the human `exportUrl` to watch. For one change, **`requestVideoEdit`** with only that change in `note` (credits again; `getVideo` goes back to rendering). When they approve it, **`approveVideo({ projectId })`** saves the MP4 to the campaign's creative library and returns its `libraryKey`.
11
+
12
+ ## Reference scripts
13
+
14
+ `listReferenceScripts` returns the reference ad scripts Content Studio offers as Concept presets for Bevyl videos, each distilled from an ad that performed: `description` (what the video shows), `videoUrl` (a public MP4 preview), `structure` (the ordered beats), `keyPhrases` (lines to adapt, with `<placeholders>` filled from the campaign's facts) and `concept` (the exact text Content Studio sends to Bevyl). Copy the structure and pacing, not the words. The list is the same for every organization. Use one to explain a concept option or to write a concept of your own for `prompt`.