@wrangled/cli 0.3.8 → 0.3.11
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 +10 -3
- package/dist/main.js +65 -4
- package/dist/playbooks.js +3 -3
- package/package.json +1 -1
package/dist/guide.js
CHANGED
|
@@ -62,7 +62,7 @@ For "how is the funnel doing?", "which campaigns are working?" and "where does t
|
|
|
62
62
|
|
|
63
63
|
## Webhook addresses
|
|
64
64
|
|
|
65
|
-
A webhook address lets a tool such as a form or webinar platform push events into the warehouse's \`raw_events\` table. \`${cli} intake list\` shows each address with whether it is on, how many days it keeps events, the events of the last 24 hours, the last event's time, and the rows it holds. \`${cli} intake create <label>\` makes one and prints its link, address and token. The link is the one URL to paste into the sending tool: the address with the token on the end, or for Mailgun the bare address. Add \`--kind ghl_workflow\` for a GoHighLevel workflow's Webhook step: its events also land, typed, in \`ghl_events\`. Add \`--kind aloware\` for Aloware's call and text webhooks: they also land, typed, in \`aloware_calls\` and \`aloware_texts\`. Add \`--kind webinarfuel\` or \`--kind stealth_seminar\` for a webinar tool: its sign-ups, attendance, replays and offer clicks land in \`webinar_events\`. Add \`--kind mailgun --signing-key-stdin\`, with Mailgun's HTTP webhook signing key piped on stdin, for a Mailgun account's webhooks: each event's signature is checked and alone proves the sender, so Mailgun posts to the address itself with no token, and its events also land, typed, in \`mailgun_events\`. When the account's key changes, pipe the new one to \`${cli} intake signing-key <id> --signing-key-stdin\`; deleting the workspace's last Mailgun address deletes the key. The link and token are shown only then and after \`${cli} intake rotate <id>\`. Paste the link into the sending tool, or, when the tool can set a header, give it the address and \`Authorization: Bearer <token>\` instead, which keeps the token out of request logs. Never paste either anywhere else. Events are kept for 90 days unless \`--retention-days\` or \`${cli} intake retention <id> <days>\` says otherwise (1 to 3650). \`${cli} intake delete <id> --yes\` deletes the address and every event it saved. A website visit tag shows in \`${cli} intake list\` as an address of kind \`visit_tag\`, and each page view it records lands in \`site_visits\` (host, path, referrer host, UTM tags, click ids, country, device type, a bot flag and a random visitor id). \`${cli} intake create\` cannot make one: a tag has no token and needs its list of allowed sites, so an operator makes it with the server's \`npm run visit-tag:create\` script.
|
|
65
|
+
A webhook address lets a tool such as a form or webinar platform push events into the warehouse's \`raw_events\` table. \`${cli} intake list\` shows each address with whether it is on, how many days it keeps events, the events of the last 24 hours, the last event's time, and the rows it holds. \`${cli} intake create <label>\` makes one and prints its link, address and token. The link is the one URL to paste into the sending tool: the address with the token on the end, or for Mailgun the bare address. Add \`--kind ghl_workflow\` for a GoHighLevel workflow's Webhook step: its events also land, typed, in \`ghl_events\`. Add \`--kind aloware\` for Aloware's call and text webhooks: they also land, typed, in \`aloware_calls\` and \`aloware_texts\`. Add \`--kind webinarfuel\` or \`--kind stealth_seminar\` for a webinar tool: its sign-ups, attendance, replays and offer clicks land in \`webinar_events\`. Add \`--kind mailgun --signing-key-stdin\`, with Mailgun's HTTP webhook signing key piped on stdin, for a Mailgun account's webhooks: each event's signature is checked and alone proves the sender, so Mailgun posts to the address itself with no token, and its events also land, typed, in \`mailgun_events\`. When the account's key changes, pipe the new one to \`${cli} intake signing-key <id> --signing-key-stdin\`; deleting the workspace's last Mailgun address deletes the key. Add \`--kind thrivecart_events\` for a ThriveCart account's webhook: affiliate commissions earned, taken back and paid out also land, typed, in \`thrivecart_affiliate_commissions\`, where they stay after their events pass the retention, and \`daily_spend\` counts them. The link and token are shown only then and after \`${cli} intake rotate <id>\`. Paste the link into the sending tool, or, when the tool can set a header, give it the address and \`Authorization: Bearer <token>\` instead, which keeps the token out of request logs. Never paste either anywhere else. Events are kept for 90 days unless \`--retention-days\` or \`${cli} intake retention <id> <days>\` says otherwise (1 to 3650). \`${cli} intake delete <id> --yes\` deletes the address and every event it saved. A website visit tag shows in \`${cli} intake list\` as an address of kind \`visit_tag\`, and each page view it records lands in \`site_visits\` (host, path, referrer host, UTM tags, click ids, country, device type, a bot flag and a random visitor id). \`${cli} intake create\` cannot make one: a tag has no token and needs its list of allowed sites, so an operator makes it with the server's \`npm run visit-tag:create\` script.
|
|
66
66
|
|
|
67
67
|
## Limits
|
|
68
68
|
|
|
@@ -135,7 +135,7 @@ The \`touches\` view reads the SKU back into its \`sku\` column, and \`item_resu
|
|
|
135
135
|
|
|
136
136
|
A launch plan writes down one campaign push before you make things for it, and keeps those things together: what the push is for, where it runs, the landing page its ads send people to, and the saved items that belong to it. Nothing in a plan is sent anywhere.
|
|
137
137
|
|
|
138
|
-
1. \`${cli} plans create --name "P1 cold, guarantee page" --goal "Five new static ads" --channel meta --target '{"account_id": "act_1", "campaign_id": "<id>", "adset_id": "<id>"}' --landing-url https://example.com/guarantee\` makes a draft plan and prints its id. Channels are \`meta\`, \`ghl\`, \`klaviyo\` and \`web\`. A meta plan's target uses ids from \`meta_campaigns\` and \`meta_adsets\`, never names: \`adset_id\` adds the ads to that ad set, \`template_adset_id\` instead copies that ad set for the push. The campaign and ad set must be synced and belong together, and with \`--run <agent-run-id>\` they must be among the campaigns your agent may touch. \`--landing <sku>\` names a landing page made here instead of an address. \`--funnel <key> --step <key>\` says which step of a funnel on the confirmed map
|
|
138
|
+
1. \`${cli} plans create --name "P1 cold, guarantee page" --goal "Five new static ads" --channel meta --target '{"account_id": "act_1", "campaign_id": "<id>", "adset_id": "<id>"}' --landing-url https://example.com/guarantee\` makes a draft plan and prints its id. Channels are \`meta\`, \`ghl\`, \`klaviyo\` and \`web\`. A meta plan's target uses ids from \`meta_campaigns\` and \`meta_adsets\`, never names: \`adset_id\` adds the ads to that ad set, \`template_adset_id\` instead copies that ad set for the push. The campaign and ad set must be synced and belong together, and with \`--run <agent-run-id>\` they must be among the campaigns your agent may touch. \`--landing <sku>\` names a landing page made here instead of an address. \`--funnel <key> --step <key>\` says which step of a funnel on the confirmed map this push itself is (not where its clicks land), such as \`--funnel webinar_to_course --step ad\`; keys are in \`${cli} funnels show\`.
|
|
139
139
|
2. \`${cli} content create --kind ad ... --plan <plan-id>\` saves a new item straight into the plan. \`${cli} plans add <plan-id> <sku>\` adds one already saved; an item may sit in several plans. The role defaults from the item's kind (\`ad\`, \`email\`, \`sms\`, \`landing_page\`, \`video\`, or \`post\` for a social post); \`--role copy\` or \`--role image\` says otherwise, and \`--position <n>\` sets its place. \`${cli} plans remove <plan-id> <sku>\` takes it out.
|
|
140
140
|
3. \`${cli} plans status <plan-id> ready\` moves the plan one step along \`draft\`, \`ready\`, \`publishing\`, \`live\`, \`done\`. \`cancelled\` ends a plan that is not done. A done or cancelled plan cannot be edited.
|
|
141
141
|
4. \`${cli} plans show <plan-id>\` prints the plan and its items in order. A plan that feeds a funnel step also shows the brief: which step of which funnel, what that funnel sells, and the funnel's steps; \`--format json\` adds each step's numbers for the last 30 days. If the current map has since dropped that step, it shows the step from the map version the plan was made with, and a funnel note says so. Every plan is in the \`content_launch_plans\` and \`content_launch_plan_items\` tables for \`${cli} warehouse query\`.
|
|
@@ -166,6 +166,10 @@ Kinds are \`exclusion\`, \`gap\`, \`baseline\`, \`change\`, \`definition\`, \`ca
|
|
|
166
166
|
|
|
167
167
|
To suggest a fact, write it as JSON and run \`${cli} facts propose --file fact.json\` (or pipe it on stdin): \`{"kind": "gap", "subject_type": "table", "subject_ref": "meta_insights_daily", "key": "history_start", "value": {"table": "meta_insights_daily", "from": "2026-04-07"}, "summary": "Facebook ad numbers only go back to April 7, 2026.", "evidence": {"query": "SELECT min(date) FROM meta_insights_daily"}}\`. You only suggest: a person confirms, fixes or rejects it in the app. A fact with the same kind, subject and key already open is refused as \`fact_exists\`. Evidence is counts, queries and ids, never names, emails or phones.
|
|
168
168
|
|
|
169
|
+
## What customers keep saying
|
|
170
|
+
|
|
171
|
+
Every hour an AI reads the workspace's new Facebook and Instagram comments, Google reviews and Reddit posts and comments, pulls out openers (lines that would open an ad), objections (why people hesitate) and phrases (customers' own words for their problem or result), and groups the ones that say the same thing. Names, emails, phone numbers and handles are taken out first. \`${cli} voice\` lists the groups, most common first (\`--kind opener|objection|phrase\`, \`--limit <n>\` up to 200, \`--json\`), each with how many rows said it, their likes, and up to 3 real quotes. In \`--json\`, each quote's \`{kind, ref, quote}\` goes straight into a creative's \`researchSources\`, and a group itself can be cited as its \`ref\`, \`voice_clusters:<id>\`. The same rows are in the \`voice_clusters\` and \`voice_insights\` tables.
|
|
172
|
+
|
|
169
173
|
## The weekly story
|
|
170
174
|
|
|
171
175
|
Each workspace keeps a weekly story: a one-screen read of what the data says this week, why, what is working, what the data cannot tell us, and what to do next. \`${cli} story\` prints the newest version, \`${cli} story list\` every version newest first, and \`${cli} story show <version>\` one of them (each takes \`--json\`). Every save is a new version; nothing is ever changed or deleted.
|
|
@@ -225,7 +229,7 @@ ${cli} sources list list data sources and their health
|
|
|
225
229
|
${cli} sources show <id> show one source's health, tables and last sync runs
|
|
226
230
|
${cli} sources settings <id> [<key>=<value> ...] show a key source's settings, or check them with its saved key and save them
|
|
227
231
|
${cli} intake list list webhook addresses and their health
|
|
228
|
-
${cli} intake create <label> [--kind generic|ghl_workflow|aloware|webinarfuel|stealth_seminar|mailgun] [--retention-days <n>] [--signing-key-stdin] make a webhook address; prints its link and token once
|
|
232
|
+
${cli} intake create <label> [--kind generic|ghl_workflow|aloware|webinarfuel|stealth_seminar|mailgun|thrivecart_events] [--retention-days <n>] [--signing-key-stdin] make a webhook address; prints its link and token once
|
|
229
233
|
${cli} intake rotate <id> replace an address's token; prints the new link and token once
|
|
230
234
|
${cli} intake signing-key <id> --signing-key-stdin replace the workspace's signing key for that address's kind (mailgun)
|
|
231
235
|
${cli} intake disable <id> | enable <id> turn an address off or back on
|
|
@@ -268,6 +272,9 @@ ${cli} funnels evidence prices paid, buyer paths, GoHighLevel funnels, calenda
|
|
|
268
272
|
${cli} funnels draft --file <map.json> [--note <why>] [--evidence '<json>' | @file.json] [--based-on <n>] [--counts-from fix|<date>] [--questions @file.json] save the whole map; it goes live when every step can be counted
|
|
269
273
|
${cli} facts [--kind <kind>] [--subject <type>[:<ref>]] [--status <status>] [--json] | facts show <id> settled facts about the data, to treat as true
|
|
270
274
|
${cli} facts propose --file <fact.json> suggest a fact for a person to confirm
|
|
275
|
+
${cli} voice [--kind opener|objection|phrase] [--limit <n>] [--json] what customers keep saying, grouped and counted, with real quotes
|
|
276
|
+
${cli} voice lists where that voice comes from: the competitors whose ads, and the subreddits and Reddit search phrases whose posts, are saved, and suggestions waiting
|
|
277
|
+
${cli} voice suggest competitor|subreddit|search_phrase "<value>" [--note <why>] suggest a competitor, subreddit or Reddit search phrase for a person to approve
|
|
271
278
|
${cli} story [--json] | story list | story show <version> the weekly story, its versions, and the rows behind each sentence
|
|
272
279
|
${cli} story submit --file <story.json> save the next version of the weekly story
|
|
273
280
|
${cli} worker list | worker runs <hand-id | skill> [--limit <n>] | worker run-now <hand-id | skill> this workspace's scheduled hands, their runs and costs, and Run now
|
package/dist/main.js
CHANGED
|
@@ -1099,6 +1099,24 @@ const commands = {
|
|
|
1099
1099
|
console.log(renderTable(FACT_COLUMNS, facts.map(factRow)));
|
|
1100
1100
|
},
|
|
1101
1101
|
},
|
|
1102
|
+
voice: {
|
|
1103
|
+
usage: "voice [--kind opener|objection|phrase] [--limit <n>] [--json] what customers keep saying, most common first",
|
|
1104
|
+
async run(args, options) {
|
|
1105
|
+
if (args.length > 0)
|
|
1106
|
+
throw new CliError("Usage: voice [--kind opener|objection|phrase] [--limit <n>] [--json]", exitCodes.usage);
|
|
1107
|
+
const query = new URLSearchParams();
|
|
1108
|
+
if (options.kind)
|
|
1109
|
+
query.set("kind", options.kind);
|
|
1110
|
+
if (options.limit)
|
|
1111
|
+
query.set("limit", options.limit);
|
|
1112
|
+
const { clusters } = await api(await resolveConnection(options.apiUrl), `${VOICE_PATH}${query.size ? `?${query}` : ""}`);
|
|
1113
|
+
if (options.format === "json")
|
|
1114
|
+
return printJson({ clusters });
|
|
1115
|
+
if (clusters.length === 0)
|
|
1116
|
+
return console.log("Nothing grouped yet.");
|
|
1117
|
+
console.log(renderTable(VOICE_COLUMNS, clusters.map(voiceRow)));
|
|
1118
|
+
},
|
|
1119
|
+
},
|
|
1102
1120
|
"facts show": {
|
|
1103
1121
|
usage: "facts show <id> one fact with its value and evidence",
|
|
1104
1122
|
async run(args, options) {
|
|
@@ -1121,6 +1139,42 @@ const commands = {
|
|
|
1121
1139
|
console.log(`Proposed fact ${fact.id}. A person confirms or rejects it in the app.`);
|
|
1122
1140
|
},
|
|
1123
1141
|
},
|
|
1142
|
+
"voice lists": {
|
|
1143
|
+
usage: "voice lists where the voice comes from: the competitors whose ads, and the subreddits and Reddit search phrases whose posts, this workspace saves, and the suggestions waiting",
|
|
1144
|
+
async run(args, options) {
|
|
1145
|
+
if (args.length > 0)
|
|
1146
|
+
throw new CliError("Usage: voice lists", exitCodes.usage);
|
|
1147
|
+
const lists = await api(await resolveConnection(options.apiUrl), VOICE_LISTS_PATH);
|
|
1148
|
+
if (options.format === "json")
|
|
1149
|
+
return printJson(lists);
|
|
1150
|
+
const rows = [
|
|
1151
|
+
...lists.competitors.map((c) => ({ list: "competitor", entry: `${c.name} (Page ${c.pageId})`, status: "listed", note: null })),
|
|
1152
|
+
...lists.subreddits.map((s) => ({ list: "subreddit", entry: `r/${s}`, status: "listed", note: null })),
|
|
1153
|
+
...lists.phrases.map((p) => ({ list: "search_phrase", entry: p, status: "listed", note: null })),
|
|
1154
|
+
...lists.suggestions.map((s) => ({ list: s.kind, entry: s.kind === "subreddit" ? `r/${s.value}` : s.value, status: "suggested", note: s.note })),
|
|
1155
|
+
];
|
|
1156
|
+
if (rows.length === 0)
|
|
1157
|
+
return console.log("No competitors, subreddits or phrases yet, and no suggestions.");
|
|
1158
|
+
console.log(renderTable(["list", "entry", "status", "note"], rows));
|
|
1159
|
+
},
|
|
1160
|
+
},
|
|
1161
|
+
"voice suggest": {
|
|
1162
|
+
usage: "voice suggest competitor|subreddit|search_phrase <value> [--note <why>] suggest a competitor, subreddit or Reddit search phrase; a person approves it in the app",
|
|
1163
|
+
async run(args, options) {
|
|
1164
|
+
const [kind, value, ...rest] = args;
|
|
1165
|
+
if (!kind || !["competitor", "subreddit", "search_phrase"].includes(kind) || value === undefined || rest.length > 0) {
|
|
1166
|
+
throw new CliError('Usage: voice suggest competitor|subreddit|search_phrase "<value>" [--note <why>]; put a value with spaces in quotes', exitCodes.usage);
|
|
1167
|
+
}
|
|
1168
|
+
const result = await api(await resolveConnection(options.apiUrl), VOICE_SUGGESTIONS_PATH, { kind, value, note: options.note });
|
|
1169
|
+
if (options.format === "json")
|
|
1170
|
+
return printJson(result);
|
|
1171
|
+
if (result.outcome === "already_listed")
|
|
1172
|
+
return console.log(`That ${kind.replace("_", " ")} is already on the list.`);
|
|
1173
|
+
if (result.outcome === "already_suggested")
|
|
1174
|
+
return console.log(`That ${kind.replace("_", " ")} is already suggested (${result.suggestion.id}), waiting for a person.`);
|
|
1175
|
+
console.log(`Suggested ${kind.replace("_", " ")} ${result.suggestion.value} (${result.suggestion.id}). A person approves or dismisses it in the app.`);
|
|
1176
|
+
},
|
|
1177
|
+
},
|
|
1124
1178
|
story: {
|
|
1125
1179
|
usage: "story the newest weekly story, with the rows behind each sentence",
|
|
1126
1180
|
async run(args, options) {
|
|
@@ -1564,7 +1618,11 @@ function printReport(title, report, labels, numbers) {
|
|
|
1564
1618
|
}
|
|
1565
1619
|
const agentPath = (options) => `/api/v1/agents/${encodeURIComponent(options.agent)}`;
|
|
1566
1620
|
const FACTS_PATH = "/api/v1/facts";
|
|
1621
|
+
const VOICE_LISTS_PATH = "/api/v1/voice-lists";
|
|
1622
|
+
const VOICE_SUGGESTIONS_PATH = "/api/v1/voice-lists/suggestions";
|
|
1567
1623
|
const FACT_COLUMNS = ["id", "kind", "subject", "key", "status", "summary"];
|
|
1624
|
+
const VOICE_PATH = "/api/v1/voice";
|
|
1625
|
+
const VOICE_COLUMNS = ["ref", "kind", "said by", "likes", "label", "top quote"];
|
|
1568
1626
|
function knowledgeBlock(r, i) {
|
|
1569
1627
|
const lines = [`${i + 1}. ${r.title}`, ` ${r.kind}, ${r.evidence.replaceAll("_", " ")}`];
|
|
1570
1628
|
if (r.url)
|
|
@@ -1574,6 +1632,9 @@ function knowledgeBlock(r, i) {
|
|
|
1574
1632
|
lines.push(` Caveat: ${r.caveat}`);
|
|
1575
1633
|
return lines.join("\n");
|
|
1576
1634
|
}
|
|
1635
|
+
function voiceRow(c) {
|
|
1636
|
+
return { ref: c.ref, kind: c.kind, "said by": c.supportCount, likes: c.likeSum, label: excerpt(c.label, 60), "top quote": c.evidence[0] ? excerpt(c.evidence[0].quote, 60) : null };
|
|
1637
|
+
}
|
|
1577
1638
|
function factRow(f) {
|
|
1578
1639
|
return { id: f.id, kind: f.kind, subject: `${f.subjectType}:${f.subjectRef}`, key: f.key, status: f.status, summary: excerpt(f.summary, 80) };
|
|
1579
1640
|
}
|
|
@@ -1634,7 +1695,7 @@ Options:
|
|
|
1634
1695
|
--wait <seconds> how long tools run and tools get wait for a run (default: ${DEFAULT_WAIT_SECONDS}; 0 returns at once)
|
|
1635
1696
|
--agent <kind> which agent the agent commands use: meta_ads (default) or p3_refresh; in a scheduled hand's run, skill
|
|
1636
1697
|
(the run's skill's agent) is the default
|
|
1637
|
-
--kind <kind> what sends to a new intake address: generic (default), ghl_workflow, aloware, webinarfuel, stealth_seminar or
|
|
1698
|
+
--kind <kind> what sends to a new intake address: generic (default), ghl_workflow, aloware, webinarfuel, stealth_seminar, mailgun or thrivecart_events
|
|
1638
1699
|
--retention-days <n> how many days intake create keeps each event (default: 90)
|
|
1639
1700
|
--signing-key-stdin read the sender's signing key for intake create or intake signing-key from stdin (mailgun needs one)
|
|
1640
1701
|
--table <name> the table import csv loads the file into, or import remove drops, as import_<name>;
|
|
@@ -1676,8 +1737,8 @@ Options:
|
|
|
1676
1737
|
--questions <json | @file.json> the questions funnels draft asks a person, as an array
|
|
1677
1738
|
--diff <a> <b> with funnels versions, what changed from version a to version b
|
|
1678
1739
|
--funnel <key> only this funnel's steps in funnels numbers (default: every funnel in the confirmed map);
|
|
1679
|
-
with plans create and --step, the funnel
|
|
1680
|
-
--step <key> with plans create, the step of --funnel
|
|
1740
|
+
with plans create and --step, the funnel the push is a step of
|
|
1741
|
+
--step <key> with plans create, the step of --funnel this push itself is, as on the confirmed map
|
|
1681
1742
|
--harness <list> which agents skill publish marks the skill for: claude, codex, or claude,codex (default)
|
|
1682
1743
|
--text <words> words content search finds in a subject, headline, body or video transcript
|
|
1683
1744
|
--mode word|meaning how content search matches --text: word (default, the words themselves) or meaning (close in meaning, nearest first; no --sort)
|
|
@@ -1686,7 +1747,7 @@ Options:
|
|
|
1686
1747
|
--from, --to <n> with content diff, the two versions to compare (default: the one before the current one, and the current one)
|
|
1687
1748
|
--sort <column> how content search sorts: recent (default) or a content_scorecard number, such as click_rate or roas
|
|
1688
1749
|
--order desc|asc content search's sort direction (default: desc, best or newest first)
|
|
1689
|
-
--limit <n> how many results content search, creatives list and
|
|
1750
|
+
--limit <n> how many results content search, creatives list, campaigns and voice return
|
|
1690
1751
|
--max-seconds <n> stop a runner's run after this many seconds, if that comes before its deadline
|
|
1691
1752
|
--workspace <slug> for worker promote and worker drop: the workspace whose hand they change
|
|
1692
1753
|
-h, --help show this help
|
package/dist/playbooks.js
CHANGED
|
@@ -595,7 +595,7 @@ ${cli} funnels show --format json > current.json
|
|
|
595
595
|
|
|
596
596
|
\`evidence.json\` is \`{"evidence": {...}, "text": "..."}\`. The parts below are under \`.evidence\`, each either \`{"status": "ok", "rows": [...]}\` or \`{"status": "not_connected", "note": "..."}\`. Amounts are in whole currency units (1500 is 1,500.00), over the last \`.evidence.windowDays\` days unless a part says otherwise.
|
|
597
597
|
|
|
598
|
-
- **prices**: the prices people pay, on each platform, with how many buyers and payments. A GoHighLevel or Shopify row has the product's \`name\`; a Stripe charge has none. For GoHighLevel, \`payments\` counts orders, not instalments. A GoHighLevel sale taken through Stripe shows twice, as a
|
|
598
|
+
- **prices**: the prices people pay, on each platform, with how many buyers and payments. A GoHighLevel or Shopify row has the product's \`name\`, a Kajabi row the offer's title, with the Kajabi offer id as \`productId\`; a Stripe charge has none. For GoHighLevel, \`payments\` counts orders, not instalments. A GoHighLevel or Kajabi sale taken through Stripe shows twice, as a row naming the product and a Stripe row with only the amount, and the two amounts can differ a little (1497 and 1500).
|
|
599
599
|
- **buyerPaths**: the most common orders in which one person paid, with the median days between each payment. A small payment minutes after a big one is an order bump or upsell; a large payment weeks later is a back-end offer. A path such as \`[1500, 497, 3500]\` with medians \`[0, 60]\` reads: course, then an upsell the same day, then an event about two months later. A GoHighLevel checkout with an order bump is one payment of both prices together: 1994 is 1497 + 497. Before you take an amount for a new offer, check whether it is a sum of prices you already know.
|
|
600
600
|
- **funnels**, **calendars**, **pipelineStages**, **tags**: what GoHighLevel holds. Tags often name purchases and memberships.
|
|
601
601
|
- **webinars**: sign-ups, viewers and offer clicks per webinar id.
|
|
@@ -641,8 +641,8 @@ Start from the shape that fits, then rename and add steps to match what you read
|
|
|
641
641
|
|
|
642
642
|
Write \`map.json\` as \`{"offers": [...], "funnels": [...]}\`, in this order.
|
|
643
643
|
|
|
644
|
-
1. **Offers**, front end first, numbered by \`stack_position\` from 1. Each has an \`offer_key\` (lowercase, digits and underscores), \`name\`, \`tier\` (\`front_end\` or \`back_end\`), \`delivery\` (course, event, coaching, service, product, membership, webinar, lead_magnet or other), \`price\`, \`currency\`, an optional \`price_note\` (plain words on the price: other prices, payment plans, doubts), and \`matches\`: how a sale is recognised. A match is \`{"platform": ..., "match_type": ..., "value": "..."}\`: platform \`stripe\`, \`ghl\` or \`
|
|
645
|
-
- **Price.** A free webinar's \`price\` is 0 when its page or GoHighLevel says it is free. When GoHighLevel and Stripe both show a sale, \`price\` is the GoHighLevel price that names the product, \`price_note\` gives the Stripe amount, and the offer matches both: a GoHighLevel name match and the Stripe \`amount\`. Any other price you cannot find in the evidence is \`null\`, with a \`price_note\` saying why.
|
|
644
|
+
1. **Offers**, front end first, numbered by \`stack_position\` from 1. Each has an \`offer_key\` (lowercase, digits and underscores), \`name\`, \`tier\` (\`front_end\` or \`back_end\`), \`delivery\` (course, event, coaching, service, product, membership, webinar, lead_magnet or other), \`price\`, \`currency\`, an optional \`price_note\` (plain words on the price: other prices, payment plans, doubts), and \`matches\`: how a sale is recognised. A match is \`{"platform": ..., "match_type": ..., "value": "..."}\`: platform \`stripe\`, \`ghl\`, \`shopify\` or \`kajabi\`, and match_type \`product_id\`, \`price_id\`, \`amount\` (whole units, with a \`currency\`), \`name_contains\` (any part of the product's name, ignoring case) or \`name_starts_with\` (the start of the product's name, ignoring case). Use \`name_starts_with\` for a family of products whose names begin with one code, such as event tickets named "OCA Dallas" and "OCA Austin"; \`name_contains\` "OCA" would also catch "Local Coaching". Stripe charges carry no product, so match them by \`amount\`. For Kajabi, \`product_id\` is the offer id and a name match reads the offer's title; Kajabi has no \`price_id\`. Prefer a GoHighLevel or Shopify name match on the product's name when one price is shared by two offers, as a $497 bump and a $497 guest ticket can be. An offer nobody pays for, such as a free webinar, may have no \`matches\`.
|
|
645
|
+
- **Price.** A free webinar's \`price\` is 0 when its page or GoHighLevel says it is free. When GoHighLevel or Kajabi and Stripe both show a sale, \`price\` is the GoHighLevel or Kajabi price that names the product, \`price_note\` gives the Stripe amount, and the offer matches both: a GoHighLevel name match or the Kajabi offer, and the Stripe \`amount\`. A sale Stripe holds counts only under Stripe. Any other price you cannot find in the evidence is \`null\`, with a \`price_note\` saying why.
|
|
646
646
|
2. **Funnels and steps.** A funnel has a \`funnel_key\`, \`name\`, \`kind\` (\`front_end\` or \`back_end\`), \`entry_offer_key\`, \`window_days\` (how many days after one step a sale still counts for the next; 365 when left out) and \`steps\`. A step has a \`step_key\`, \`position\` (1, 2, 3 in the order people walk them), \`step_type\`, \`name\`, and optionally the \`offer_key\` it sells, a \`wait_note\` (plain words on the wait before it, such as "the months after the course"), \`assets\` and \`rules\`.
|
|
647
647
|
- Step types: \`ad\`, \`landing_page\`, \`vsl\`, \`optin\`, \`application\`, \`booking\`, \`confirmation\`, \`sales_call\`, \`checkout\`, \`order_bump\`, \`upsell\`, \`downsell\`, \`thank_you\`, \`webinar_registration\`, \`webinar\`, \`event\`, \`email_nurture\`, \`sms_nurture\`, \`onboarding\`, \`other\`.
|
|
648
648
|
3. **Assets** on each step: \`{"asset_type": ..., "ref": ...}\` with refs from the evidence: \`meta_campaign\` (campaign id), \`url\` (page address), \`content_item\` (content item id), \`ghl_funnel_step\` (step id), \`ghl_calendar\`, \`ghl_pipeline_stage\`, \`ghl_tag\`, \`webinar\` (webinar id). Give the step after Traffic its page, the funnel's first page, as a \`url\`, a \`content_item\` from \`pages\` or a \`ghl_funnel_step\`: the Traffic step finds its ads from it. Only that step's pages count, so a checkout page that two funnels share is never mistaken for a first page.
|