@wrangled/cli 0.3.41 → 0.3.43
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/guide.js +5 -2
- package/dist/main.js +52 -13
- package/dist/playbooks.js +9 -3
- package/package.json +1 -1
package/dist/guide.js
CHANGED
|
@@ -40,7 +40,7 @@ ${brand.name} does not build pages; it hands whoever builds one what is already
|
|
|
40
40
|
|
|
41
41
|
## Which Facebook ads and pages won: winners and flops
|
|
42
42
|
|
|
43
|
-
\`${cli} winners\` says which Facebook ads won and which flopped, for the dates asked for and for the 30 days ending on their last day, judged on real sales: each sale goes to the Facebook ad that first brought the buyer in, and payments typed into GoHighLevel by hand never count toward an ad. An ad with no sales that has spent less than twice the account's cost per sale is too new to judge on sales, so it is judged on cost per first lead and listed apart. Each ad says in a sentence why it is on its list. Each currency also says how many of its Facebook sales could be tied to a specific ad (\`adSales\` of \`facebookSales\` in the JSON): a sale whose link names no single ad counts toward Facebook but toward no ad. It judges the landing pages the ads sent people to the same way, and each ad on each page as an ad-and-page pair: a page or pair counts only once the page reader has read the page and it got at least 100 visits in the dates, and pairs are listed only for an ad that sent people to two or more pages, or a page that got people from two or more ads, each naming what the same ad cost on its other pages. \`--week <YYYY-MM-DD>\` picks any day of a Monday-to-Sunday week (default: the latest complete week), \`--days <n>\` the last n complete days ending yesterday, and \`--from <YYYY-MM-DD> --to <YYYY-MM-DD>\` any dates; use one of them, at most 90 days. Check as often as suits the client, daily or every few days: a short window holds few sales, so more ads are judged on first leads, and the report says so. \`--top <n>\` how many ads, pages or pairs each list shows (default 5, at most 20), and \`--format json\` gives the report as data.
|
|
43
|
+
\`${cli} winners\` says which Facebook ads won and which flopped, for the dates asked for and for the 30 days ending on their last day, judged on real sales: each sale goes to the Facebook ad that first brought the buyer in, and payments typed into GoHighLevel by hand never count toward an ad. An ad with no sales that has spent less than twice the account's cost per sale is too new to judge on sales, so it is judged on cost per first lead and listed apart. Each ad says in a sentence why it is on its list. Each currency also says how many of its Facebook sales could be tied to a specific ad (\`adSales\` of \`facebookSales\` in the JSON): a sale whose link names no single ad counts toward Facebook but toward no ad. It judges the landing pages the ads sent people to the same way, and each ad on each page as an ad-and-page pair: a page or pair counts only once the page reader has read the page and it got at least 100 visits in the dates, and pairs are listed only for an ad that sent people to two or more pages, or a page that got people from two or more ads, each naming what the same ad cost on its other pages. \`--week <YYYY-MM-DD>\` picks any day of a Monday-to-Sunday week (default: the latest complete week), \`--days <n>\` the last n complete days ending yesterday, and \`--from <YYYY-MM-DD> --to <YYYY-MM-DD>\` any dates; use one of them, at most 90 days. Check as often as suits the client, daily or every few days: a short window holds few sales, so more ads are judged on first leads, and the report says so. \`--top <n>\` how many ads, pages or pairs each list shows (default 5, at most 20), \`--ad-format image|video|carousel\` ranks only ads with that format tag (run \`${cli} ads tag-formats\` first; the account's cost per sale still counts every ad), and \`--format json\` gives the report as data.
|
|
44
44
|
|
|
45
45
|
## What winners and flops taught: lessons
|
|
46
46
|
|
|
@@ -227,6 +227,8 @@ Ads not made here (built by hand or by an agency, names in no pattern) get the m
|
|
|
227
227
|
|
|
228
228
|
To suggest tags, write them as JSON and run \`${cli} ads tag --file tags.json\` (or pipe it on stdin), up to 500 ads at a time: \`{"ads": [{"ad_id": "120214567890123", "tags": {"angle": "discovery", "desire": "money", "hook_spot": "spoken", "hook_text": "$29,000 is the smallest lead", "hook_kind": "big_win", "format": "video", "style": "presenter", "tag_reasons": {"worry": "none"}}, "confidence": "medium", "evidence": {"ad_name": "County lead v2", "transcript_start": "$29,000 is the smallest lead"}}]}\`. The tag fields are \`marketing\`, \`offer_key\`, \`landing_funnel_key\`, \`landing_step_key\`, \`persona\`, \`awareness\`, \`concept\`, \`angle\`, \`desire\`, \`hook_spot\`, \`hook_text\`, \`hook_kind\`, \`hook2_spot\`, \`hook2_kind\`, \`format\`, \`style\`, \`speaker\`, \`cta\`, \`proof\` and \`worry\`; leave out what you did not judge. A part with no value goes in \`tag_reasons\` as \`none\` (nothing on its list applies) or \`unclear\` (too little to judge, never \`high\` confidence). Confidence is \`high\`, \`medium\` or \`low\`. A value off its list is refused with the allowed values. New tags replace an ad's proposed ones, which stay as \`replaced\`; an ad whose tags a person confirmed is kept as it is.
|
|
229
229
|
|
|
230
|
+
Start with \`${cli} ads tag-formats\`: it tags every untagged ad's format (image, video or carousel) from Meta's own records and the content library, with no model call and no cost, so \`${cli} winners --ad-format image\` works at once. It goes through the ads in batches, printing progress, until none is left; running it again tags nothing new. Its rows are proposed, high confidence, with \`evidence.tagger\` \`meta_format\`, and hold only the format: \`${cli} ads autotag\` still tags those ads and keeps the format, and so does a proposal of yours that leaves format out.
|
|
231
|
+
|
|
230
232
|
To have the server tag ads for you, run \`${cli} ads autotag\` (\`--limit <n>\`, 1 to 100, default 25; \`--ad <id>\` for one ad; \`--json\`). It takes the biggest spenders with no tags yet, so each run moves on to the next ones (with \`--ad\`, that ad, its proposed tags asked about again and confirmed ones kept), reads each one's copy, video transcript and picture description, and asks TypeSafe's Jev the grammar's questions through the tool gateway, so a run's tool cap applies. Ads with the same copy, headlines, button, link and media are asked about once and share the tags; asking again about an unchanged ad costs nothing. It costs a tiny fraction of a cent per ad. The tags are proposed, with each part's confidence in the evidence; read them with \`${cli} ads tags\`. The \`back-tag\` playbook (\`${cli} skill install back-tag\` installs it as the \`${BACK_TAG_SKILL_NAME}\` skill; \`${cli} docs back-tag\` prints it) runs it in batches over a client's old ads and reports how much of the spend is tagged; a person then checks the ads carrying the top 20% of spend on the app's Tag check page.
|
|
231
233
|
|
|
232
234
|
For what worked, query the \`ad_performance_tagged\` view: every Meta ad's daily results with its live tags next to them, \`tags_source\` (\`ad_tags\`, or null when untagged) and \`tags_status\`.
|
|
@@ -299,7 +301,7 @@ ${cli} brief [--from <date>] [--to <date>] [--channel <channel>] [--campaign <id
|
|
|
299
301
|
${cli} pages brief --step <key> [--funnel <key>] | --page <ref> the offer, step, current page, best and worst pages of its kind, its ads and how to hand a page back: start here before writing a landing page
|
|
300
302
|
${cli} pages results [--from <date>] [--to <date>] [--page <ref>] each landing page's, and each ad and page pair's, real first-touch sales, cost per sale and sales per page view
|
|
301
303
|
${cli} messages results [--from <date>] [--to <date>] [--channel email|text] [--sequence <id>] real sales after each follow-up email, text and sequence: every message the buyer clicked or replied to in the ${MESSAGE_CREDIT_DAYS} days before buying (helped), and the latest one (last nudge)
|
|
302
|
-
${cli} winners [--week <date> | --days <n> | --from <date> --to <date>] [--top <n>] which Facebook ads and landing pages won and flopped in a week or any dates and over 30 days, judged on first-touch sales
|
|
304
|
+
${cli} winners [--week <date> | --days <n> | --from <date> --to <date>] [--top <n>] [--ad-format image|video|carousel] which Facebook ads and landing pages won and flopped in a week or any dates and over 30 days, judged on first-touch sales, or on Triple Whale's orders for a shop that has them
|
|
303
305
|
${cli} winners send-to-exodus [--week <date> | --days <n> | --from <date> --to <date>] [--dry-run] send the winners, with their reasons, lessons and customer words, to Exodus; safe to send again
|
|
304
306
|
${cli} warehouse schema [--summary] [--table <name> ...] [--search <words>] list tables and columns; --summary for one row per table
|
|
305
307
|
${cli} warehouse query "<sql>" run one read-only query (or --file <path>, or stdin); --out <file.csv | file.ndjson | file.json> saves the rows; add --all for every row
|
|
@@ -358,6 +360,7 @@ ${cli} facts propose --file <fact.json> suggest a fact for a person to confirm
|
|
|
358
360
|
${cli} lessons [--status active|retired] [--about ads|pages|funnel|messages|cross_channel] [--dimension <d>] | lessons add --file <lesson.json> | lessons retire <id> --note "<why>" lessons from winners and flops, writing one, retiring one
|
|
359
361
|
${cli} tests [--status <status>] [--funnel <key>] [--limit <n>] | tests show <id> | tests draft --file <test.json> | tests start <id> | tests abandon <id> [--note "<why>"] | tests results <id> | tests decide <id> --decision <d> [--note "<why>"] funnel tests, one change each
|
|
360
362
|
${cli} ads tags [--ad <id>] [--status <status>] [--limit <n>] | ads tag --file <tags.json> the marketing tags of ads not made here, and suggesting them
|
|
363
|
+
${cli} ads tag-formats tag every untagged ad's format (image, video or carousel) from Meta's records, free; run it before ads autotag
|
|
361
364
|
${cli} ads autotag [--limit <n>] [--ad <id>] have the server propose tags for the biggest spenders not yet tagged
|
|
362
365
|
${cli} content tags [<item-id>] [--kind email|text|page] | content to-check the marketing tags of emails, texts and landing pages, and those waiting for a person
|
|
363
366
|
${cli} content autotag [<item-id> ...] [--kind email|text|page] [--limit <n>] have the server propose tags for the emails, texts and pages worth most not yet tagged
|
package/dist/main.js
CHANGED
|
@@ -681,12 +681,12 @@ const commands = {
|
|
|
681
681
|
},
|
|
682
682
|
},
|
|
683
683
|
winners: {
|
|
684
|
-
usage: "winners [--week <YYYY-MM-DD> | --days <n> | --from <YYYY-MM-DD> --to <YYYY-MM-DD>] [--top <n>] (which Facebook ads and landing pages won and flopped, judged on real first-touch
|
|
684
|
+
usage: "winners [--week <YYYY-MM-DD> | --days <n> | --from <YYYY-MM-DD> --to <YYYY-MM-DD>] [--top <n>] [--ad-format image|video|carousel] (which Facebook ads and landing pages won and flopped, judged on real sales: first-touch, or Triple Whale's orders when the workspace has them)",
|
|
685
685
|
async run(args, options) {
|
|
686
686
|
if (args.length > 0)
|
|
687
|
-
throw new CliError("Usage: winners [--week <date> | --days <n> | --from <date> --to <date>] [--top <n>]", exitCodes.usage);
|
|
687
|
+
throw new CliError("Usage: winners [--week <date> | --days <n> | --from <date> --to <date>] [--top <n>] [--ad-format image|video|carousel]", exitCodes.usage);
|
|
688
688
|
const query = new URLSearchParams();
|
|
689
|
-
for (const [name, value] of Object.entries({ ...winnersPeriod(options), top: options.top }))
|
|
689
|
+
for (const [name, value] of Object.entries({ ...winnersPeriod(options), top: options.top, format: options.entryTags.format }))
|
|
690
690
|
if (value !== undefined)
|
|
691
691
|
query.set(name, value);
|
|
692
692
|
const report = await api(await resolveConnection(options.apiUrl), `/api/v1/winners${query.size ? `?${query}` : ""}`);
|
|
@@ -1326,6 +1326,33 @@ const commands = {
|
|
|
1326
1326
|
console.log(`Left out ${plural(result.nothingToRead)} with nothing to read: no copy, headline, transcript or picture description.`);
|
|
1327
1327
|
},
|
|
1328
1328
|
},
|
|
1329
|
+
"ads tag-formats": {
|
|
1330
|
+
usage: "ads tag-formats [--limit <n>] [--json] tag every untagged ad's format (image, video or carousel) from Meta's records; free, no model",
|
|
1331
|
+
async run(args, options) {
|
|
1332
|
+
if (args.length > 0)
|
|
1333
|
+
throw new CliError("Usage: ads tag-formats [--limit <n>] [--json]", exitCodes.usage);
|
|
1334
|
+
const connection = await resolveConnection(options.apiUrl);
|
|
1335
|
+
const total = { tagged: 0, no_format: 0 };
|
|
1336
|
+
let after = null;
|
|
1337
|
+
for (;;) {
|
|
1338
|
+
const body = { ...(after === null ? {} : { after }), ...(options.limit ? { limit: Number(options.limit) } : {}) };
|
|
1339
|
+
const batch = await api(connection, AD_FORMATS_PATH, body);
|
|
1340
|
+
total.tagged += batch.tagged;
|
|
1341
|
+
total.no_format += batch.no_format;
|
|
1342
|
+
if (options.format !== "json")
|
|
1343
|
+
console.error(`Tagged ${total.tagged} so far; ${batch.remaining} untagged ${batch.remaining === 1 ? "ad" : "ads"} left to look at.`);
|
|
1344
|
+
if (batch.remaining === 0 || batch.next === null)
|
|
1345
|
+
break;
|
|
1346
|
+
after = batch.next;
|
|
1347
|
+
}
|
|
1348
|
+
if (options.format === "json")
|
|
1349
|
+
return printJson(total);
|
|
1350
|
+
const plural = (n) => `${n} ${n === 1 ? "ad" : "ads"}`;
|
|
1351
|
+
console.log(`Tagged the format of ${plural(total.tagged)}. Each is proposed, read from Meta's records; ads autotag still adds the other tags and keeps the format.`);
|
|
1352
|
+
if (total.no_format > 0)
|
|
1353
|
+
console.log(`Left ${plural(total.no_format)} untagged: neither Meta nor the content library says their format.`);
|
|
1354
|
+
},
|
|
1355
|
+
},
|
|
1329
1356
|
"content tags": {
|
|
1330
1357
|
usage: "content tags [<item-id>] [--kind email|text|page] [--status <status>] [--limit <n>] [--json] the marketing tags of emails, texts and landing pages",
|
|
1331
1358
|
async run(args, options) {
|
|
@@ -2041,7 +2068,9 @@ function money(value, currency) {
|
|
|
2041
2068
|
return `${value.toFixed(2)} ${currency}`;
|
|
2042
2069
|
}
|
|
2043
2070
|
}
|
|
2044
|
-
|
|
2071
|
+
/** A model that splits credit between ads gives fractional counts: show tenths at most, grouped as the server writes them. */
|
|
2072
|
+
const tenths = (n) => n.toLocaleString("en-US", { maximumFractionDigits: 1 });
|
|
2073
|
+
const counted = (n, one) => `${tenths(n)} ${tenths(n) === "1" ? one : `${one}s`}`;
|
|
2045
2074
|
/** An ad's line: name, SKU and campaign, its numbers, its tags, then why it is on the list. */
|
|
2046
2075
|
function winnersAdLines(ad, currency) {
|
|
2047
2076
|
const n = ad.numbers;
|
|
@@ -2052,7 +2081,8 @@ function winnersAdLines(ad, currency) {
|
|
|
2052
2081
|
const name = [ad.name ?? `Ad ${ad.adId}`, ad.sku && `[${ad.sku}]`, ad.campaignName && `(${ad.campaignName})`].filter(Boolean).join(" ");
|
|
2053
2082
|
const tags = Object.entries(ad.tags).map(([field, value]) => `${field}: ${value}`).join(", ");
|
|
2054
2083
|
const count = onSales ? n.sales !== null && counted(n.sales, "sale") : n.signups !== null && counted(n.signups, "first lead");
|
|
2055
|
-
|
|
2084
|
+
const facebook = n.facebookResults == null ? "" : `, Facebook counts ${tenths(n.facebookResults)}`;
|
|
2085
|
+
return `- ${name}: spent ${money(n.spend, currency)}, ${count ? `${count}, ` : ""}${cost}${facebook}${tags ? `. Tags: ${tags}` : ""}\n ${ad.pickedBecause}\n`;
|
|
2056
2086
|
}
|
|
2057
2087
|
/** A page's or pair's line: the ad for a pair, the page, its visits and numbers, then why it is on the list. */
|
|
2058
2088
|
function winnersPageLines(page, currency) {
|
|
@@ -2090,7 +2120,7 @@ function winnersWindowText(title, window) {
|
|
|
2090
2120
|
: s.accountCostPerSale === null
|
|
2091
2121
|
? "no sale credited to a Facebook ad in these dates"
|
|
2092
2122
|
: `the account paid ${money(s.accountCostPerSale, s.currency)} per sale`;
|
|
2093
|
-
const tied = s.adSales === null || !s.facebookSales ? "" : ` ${s.adSales} of ${counted(s.facebookSales, "Facebook sale")} could be tied to a specific ad.`;
|
|
2123
|
+
const tied = s.adSales === null || !s.facebookSales ? "" : ` ${tenths(s.adSales)} of ${counted(s.facebookSales, "Facebook sale")} could be tied to a specific ad.`;
|
|
2094
2124
|
const tests = s.tests.map((t) => `- Test ${t.testId}${t.hypothesisId ? ` (idea ${t.hypothesisId})` : ""}: ${counted(t.ads.length, "ad")}, spent ${money(t.spend, s.currency)}, ${counted(t.sales, "sale")}` +
|
|
2095
2125
|
`${t.costPerSale === null ? "" : `, ${money(t.costPerSale, s.currency)} per sale`}\n`);
|
|
2096
2126
|
return (`\nIn ${s.currency}: ${account}.${tied} ${s.adsJudgedOnSales} judged on sales, ${s.adsJudgedOnSignups} judged on first leads.\n` +
|
|
@@ -2143,12 +2173,18 @@ function winnersSendText(sent) {
|
|
|
2143
2173
|
lines.push("Left out:", ...sent.skipped.map((s) => ` ${s.adId}: ${s.reason}`));
|
|
2144
2174
|
return `${lines.join("\n")}\n`;
|
|
2145
2175
|
}
|
|
2146
|
-
/**
|
|
2176
|
+
/** How an older server, which sent no rule, credited a sale. */
|
|
2177
|
+
const FIRST_TOUCH_SALE_RULE = "Each sale goes to the Facebook ad that first brought the buyer in; payments typed into GoHighLevel by hand never count toward an ad.";
|
|
2178
|
+
/**
|
|
2179
|
+
* The winners report as plain Markdown: what a sale means, then the dates asked for and, unless they are the same dates,
|
|
2180
|
+
* the last 30 days, each currency apart.
|
|
2181
|
+
*/
|
|
2147
2182
|
function winnersText(report) {
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
winnersWindowText(
|
|
2183
|
+
const { week, last30Days } = report;
|
|
2184
|
+
return (`# Winners and flops for ${report.workspace}${report.format ? `, ${report.format} ads only` : ""}\n\n` +
|
|
2185
|
+
`A sale means ${report.saleMeans}. ${report.saleRule ?? FIRST_TOUCH_SALE_RULE}\n` +
|
|
2186
|
+
winnersWindowText(winnersDatesTitle(report.window), week) +
|
|
2187
|
+
(week.from === last30Days.from && week.to === last30Days.to ? "" : winnersWindowText(`Last 30 days, ${last30Days.from} to ${last30Days.to}`, last30Days)));
|
|
2152
2188
|
}
|
|
2153
2189
|
function reportQuery(options, extra) {
|
|
2154
2190
|
const query = new URLSearchParams();
|
|
@@ -2247,6 +2283,7 @@ const FACTS_PATH = "/api/v1/facts";
|
|
|
2247
2283
|
const AD_TAGS_PATH = "/api/v1/ad-tags";
|
|
2248
2284
|
const KNOWLEDGE_TAGS_PATH = "/api/v1/knowledge/tags";
|
|
2249
2285
|
const AD_AUTOTAG_PATH = "/api/v1/ad-tags/autotag";
|
|
2286
|
+
const AD_FORMATS_PATH = "/api/v1/ad-tags/formats";
|
|
2250
2287
|
const SKIP_REASONS = { not_marketing: "Not marketing, so only that answer was saved", gate_unclear: "Could not tell whether it is marketing, so only that was saved", no_content: "Nothing to read", not_found: "No such ad" };
|
|
2251
2288
|
const AD_TAG_COLUMNS = ["id", "ad", "status", "confidence", "offer", "persona", "angle", "hook", "format", "author"];
|
|
2252
2289
|
const CONTENT_TAGS_PATH = "/api/v1/content-tags";
|
|
@@ -2493,10 +2530,12 @@ Options:
|
|
|
2493
2530
|
--sort <column> how content search sorts: recent (default) or a content_scorecard number, such as click_rate or roas
|
|
2494
2531
|
--order desc|asc content search's sort direction (default: desc, best or newest first)
|
|
2495
2532
|
--limit <n> how many results content search, creatives list, campaigns, voice, ads tags, content tags and content to-check
|
|
2496
|
-
return,
|
|
2533
|
+
return, how many ads ads autotag tags and how many items content autotag tags, or how many
|
|
2534
|
+
ads each ads tag-formats batch looks at (default 2000)
|
|
2497
2535
|
--library <slug> with knowledge search, only this library: creative-cartel, exodus or meta-policy
|
|
2498
2536
|
--angle, --hook-kind, --ad-format, --awareness <value> with knowledge search, only library entries tagged that way, and
|
|
2499
|
-
your own ads tagged the same way with their results; with knowledge tag, the tags to put on an entry
|
|
2537
|
+
your own ads tagged the same way with their results; with knowledge tag, the tags to put on an entry;
|
|
2538
|
+
with winners, --ad-format ranks only ads whose format tag is that one (run ads tag-formats first)
|
|
2500
2539
|
--confidence high|medium|low with knowledge tag, how sure you are of the tags
|
|
2501
2540
|
--remove with knowledge tag, take the tags off instead
|
|
2502
2541
|
--ad <id> with ads tags, only this Meta ad's tags; with ads autotag, only this ad
|
package/dist/playbooks.js
CHANGED
|
@@ -818,9 +818,15 @@ export const BACK_TAG_SKILL_NAME = `${cli}-back-tag`;
|
|
|
818
818
|
export const BACK_TAG_BATCH = 100;
|
|
819
819
|
/** Batches a run stops after unless the person asked for more. */
|
|
820
820
|
export const BACK_TAG_MAX_BATCHES = 20;
|
|
821
|
+
/** evidence.tagger of a format-only row read from Meta's records; the server's META_FORMAT_TAGGER. */
|
|
822
|
+
export const META_FORMAT_TAGGER = "meta_format";
|
|
821
823
|
/** The warehouse query the back-tag playbook reports coverage with, exported so tests run the same SQL. */
|
|
822
824
|
export const backTagQueries = {
|
|
823
|
-
|
|
825
|
+
// A proposed row read from Meta's records holds only the format, so its ad still counts as untagged.
|
|
826
|
+
coverage: `WITH a AS (
|
|
827
|
+
SELECT p.currency, p.ad_id, coalesce(sum(p.spend), 0) AS spend,
|
|
828
|
+
max(CASE WHEN p.tags_status = 'proposed' AND t.evidence->>'tagger' = '${META_FORMAT_TAGGER}' THEN NULL ELSE p.tags_status END) AS status
|
|
829
|
+
FROM ad_performance_tagged p LEFT JOIN ad_tags t ON t.id = p.tags_id GROUP BY p.currency, p.ad_id)
|
|
824
830
|
SELECT currency,
|
|
825
831
|
count(*) FILTER (WHERE spend > 0) AS ads_with_spend,
|
|
826
832
|
count(*) FILTER (WHERE spend > 0 AND status IS NOT NULL) AS ads_tagged,
|
|
@@ -867,7 +873,7 @@ It answers one row per currency, since money is never added across currencies: a
|
|
|
867
873
|
${cli} ads autotag --limit ${BACK_TAG_BATCH} --json > batch.json
|
|
868
874
|
\`\`\`
|
|
869
875
|
|
|
870
|
-
Each call takes the ${BACK_TAG_BATCH} biggest-spending ads with no tags yet, so the next call moves on to the next ones. Open \`batch.json\` after each one. An ad Jev says is not marketing (\`not_marketing\`), or can't tell about (\`gate_unclear\`), is listed under \`skipped\` but was saved with only that answer, so it counts as progress. Run it again until one of these happens:
|
|
876
|
+
Each call takes the ${BACK_TAG_BATCH} biggest-spending ads with no tags yet, so the next call moves on to the next ones. An ad whose only tag is its format, read from Meta's records by \`${cli} ads tag-formats\`, counts as not tagged yet; the tagger keeps that format. Open \`batch.json\` after each one. An ad Jev says is not marketing (\`not_marketing\`), or can't tell about (\`gate_unclear\`), is listed under \`skipped\` but was saved with only that answer, so it counts as progress. Run it again until one of these happens:
|
|
871
877
|
|
|
872
878
|
- A batch saved nothing new: \`proposed\` is empty, and every ad under \`skipped\` is \`no_content\` (nothing to read: no copy, transcript or picture description) or \`not_found\`, with the rest under \`failed\`. Asking again won't help.
|
|
873
879
|
- You have run ${BACK_TAG_MAX_BATCHES} batches, or the person's spending limit would be passed.
|
|
@@ -1558,7 +1564,7 @@ const testResultsSql = "SELECT test_id, currency, sum(spend) AS spend, sum(sales
|
|
|
1558
1564
|
const lineageSql = "SELECT facebook_ad_id, hypothesis_id, test_id, creative_id, asset_version FROM adzara_ad_lineage ORDER BY synced_at DESC LIMIT 100";
|
|
1559
1565
|
/** The commands the weekly loop shows, in order; its text is made from them, so a test runs exactly these. */
|
|
1560
1566
|
export const weeklyLoopCommands = {
|
|
1561
|
-
winners: { purpose: "the Facebook winners and flops of the dates asked for, here a week, and of the 30 days ending on their last day, judged on real first-touch
|
|
1567
|
+
winners: { purpose: "the Facebook winners and flops of the dates asked for, here a week, and of the 30 days ending on their last day, judged on real sales (first-touch, or Triple Whale's orders when the workspace has them)", argv: ["winners", "--week", "<week-monday>", "--format", "json"], mcp: "get_winners_report" },
|
|
1562
1568
|
lessons: { purpose: "what earlier weeks taught, the most often seen first", argv: ["lessons", "--format", "json"], mcp: "get_lessons" },
|
|
1563
1569
|
writeLessons: { purpose: "install the lessons playbook, then follow it to add, reinforce or retire this week's lessons", argv: ["skill", "install", "lessons"], mcp: "record_lesson" },
|
|
1564
1570
|
voice: { purpose: "fresh customer words: the most usable single lines said since that Monday", argv: ["voice", "--since", "<week-monday>", "--sort", "useful", "--json"], mcp: "get_customer_voice" },
|