@oxygen-agent/cli 1.1010.644 → 1.1010.721
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 +1 -1
- package/dist/command-manifest.js +1 -1
- package/dist/inbox-needs-reply-notice.d.ts +12 -0
- package/dist/inbox-needs-reply-notice.js +51 -0
- package/dist/index.js +186 -45
- package/dist/skills.js +48 -22
- package/node_modules/@oxygen/shared/dist/billing-anchors.d.ts +33 -2
- package/node_modules/@oxygen/shared/dist/billing-anchors.js +67 -2
- package/node_modules/@oxygen/shared/dist/billing.d.ts +63 -9
- package/node_modules/@oxygen/shared/dist/billing.js +96 -14
- package/node_modules/@oxygen/shared/dist/capability-discovery.js +11 -1
- package/node_modules/@oxygen/shared/dist/copilot-skills.generated.d.ts +4 -4
- package/node_modules/@oxygen/shared/dist/copilot-skills.generated.js +4 -4
- package/node_modules/@oxygen/shared/dist/email-hard-bounce.d.ts +25 -0
- package/node_modules/@oxygen/shared/dist/email-hard-bounce.js +27 -0
- package/node_modules/@oxygen/shared/dist/feature-gates.d.ts +6 -1
- package/node_modules/@oxygen/shared/dist/feature-gates.js +7 -1
- package/node_modules/@oxygen/shared/dist/index.d.ts +2 -1
- package/node_modules/@oxygen/shared/dist/index.js +2 -1
- package/node_modules/@oxygen/shared/dist/linkedin-sequences.d.ts +114 -0
- package/node_modules/@oxygen/shared/dist/linkedin-sequences.js +150 -0
- package/node_modules/@oxygen/shared/dist/otlp-log-sink.js +19 -2
- package/node_modules/@oxygen/shared/dist/plan-band.d.ts +118 -0
- package/node_modules/@oxygen/shared/dist/plan-band.js +147 -0
- package/node_modules/@oxygen/shared/dist/plan-limits.d.ts +131 -120
- package/node_modules/@oxygen/shared/dist/plan-limits.js +80 -71
- package/node_modules/@oxygen/shared/dist/pricing-sheet.d.ts +79 -14
- package/node_modules/@oxygen/shared/dist/pricing-sheet.js +61 -12
- package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.d.ts +4 -3
- package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.js +9 -3
- package/node_modules/@oxygen/shared/dist/process-resource.d.ts +4 -0
- package/node_modules/@oxygen/shared/dist/process-resource.js +25 -0
- package/node_modules/@oxygen/shared/dist/repricing.d.ts +130 -0
- package/node_modules/@oxygen/shared/dist/repricing.js +320 -0
- package/node_modules/@oxygen/shared/dist/sending-limits.d.ts +32 -0
- package/node_modules/@oxygen/shared/dist/sending-limits.js +49 -0
- package/node_modules/@oxygen/shared/dist/sequence-failures.js +4 -1
- package/node_modules/@oxygen/shared/dist/stripe-price-catalog.d.ts +24 -0
- package/node_modules/@oxygen/shared/dist/stripe-price-catalog.js +58 -1
- package/node_modules/@oxygen/shared/dist/table-capacity.d.ts +39 -10
- package/node_modules/@oxygen/shared/dist/table-capacity.js +68 -4
- package/node_modules/@oxygen/shared/dist/telemetry.d.ts +9 -0
- package/node_modules/@oxygen/shared/dist/telemetry.js +36 -2
- package/node_modules/@oxygen/shared/dist/trace-context.d.ts +29 -0
- package/node_modules/@oxygen/shared/dist/trace-context.js +88 -0
- package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
- package/package.json +1 -1
- package/node_modules/@oxygen/shared/dist/email-warmup-readiness.d.ts +0 -64
- package/node_modules/@oxygen/shared/dist/email-warmup-readiness.js +0 -90
|
@@ -8,14 +8,14 @@ export declare const COPILOT_SKILL_SNAPSHOTS_GENERATED: readonly [{
|
|
|
8
8
|
readonly slug: "tam-sourcing";
|
|
9
9
|
readonly title: "Playbook: TAM sourcing";
|
|
10
10
|
readonly sources: readonly ["oxygen-playbooks/playbooks/tam-sourcing.md"];
|
|
11
|
-
readonly sha256: "
|
|
12
|
-
readonly content: "---\nname: tam-sourcing\ndescription: \"Turn the workspace ICP into a sourced, scored, deduplicated account map and the buyer personas at those accounts — a bounded sample first, coverage widened only on segments the sample proved — then hand it to outbound. Load it when the whole market map is the ask.\"\n---\n\n# Playbook: TAM sourcing\n\n## Motion in one sentence\n\nThe ICP compiles into a provider-grounded sourcing plan, one bounded sample of accounts is sourced live, deduplicated and filled, every account is scored against that same ICP with its evidence on the row, the sample's band distribution decides which segments deserve coverage, coverage widens one segment at a time through the same gate, buyer personas are sourced only at accounts that passed it, and the accepted map is handed to an outbound motion. The sample before the coverage is the play; the order is companies → fit → people, never people first.\n\nRecipe and kit: `oxygen recipes show icp-to-account-map --json` (its `journey.slug` is `tam-sourcing`); the companion it hands to is `oxygen recipes show outbound-pilot-50 --json`. Read both first — `recipes show` reports which stages this workspace already has.\n\n## Stage table\n\n| # | Stage | Owner | Install / command | Inputs | Cap | Approval | Verify | Stop |\n| --- | --- | --- | --- | --- | --- | --- | --- | --- |\n| 0 | Context | Knowledge Graph | `oxygen knowledge page get icp --json`; `oxygen knowledge page get offers --json` (`oxygen knowledge status` reports the local CLI mirror, not the workspace) | `company`, `offering`, `icp` pages | — | none | pages `active`, not seed stubs; the ICP names segment, geography, size **and who is out** | a stub ICP, or no exclusion clause |\n| 1 | Plan | Tables — company-search planner | `oxygen tools enums get blitzapi industry --query <segment> --json` → `oxygen companies search plan --prompt \"<ICP sentence with EXCLUDE clause>\" --filters-json '<typed filters>' --estimate --json` | one ICP sentence; exact enum strings | `--target-count` ceiling 50,000 per plan | none — no provider call, no spend | `filter_application[].dropped`, `provider_availability`, `estimated_match_count.basis`, `recommended_live_route_id` | a filter you need lands in `dropped_constraints`; a managed primary reads `degraded: true` |\n| 2 | Scaffold | Tables — kit stage `account-sourcing` | `oxygen recipes apply icp-to-account-map --dry-run --json` → the same without `--dry-run` | — | 0 credits, no provider call, no external write | `kit-apply`: human, once for the kit | `oxygen tables describe <accounts> --json` shows `company_name`, `domain`, `company_linkedin_url`, `source`, `fit_notes` | a stage already installed — never re-install it |\n| 3 | Source the sample | Tables — company search | `oxygen companies search run --plan-json <plan> --route-id <route_id> --table <accounts> --upsert-key domain --mode dry_run --json` → `--mode live --max-pages 1 --max-credits <cap> --approved` | the plan you actually read | one page, the recipe's 25-account pilot; `<cap>` from the route estimate | `source-sample`: paid, human, server-enforced | `oxygen table-ingestions wait <ingestion_run_id> --json`; `oxygen tables query <accounts> --limit 25 --json` | `spend_cap_too_low`; near-zero rows; `data_status` still `pending` — the verdict is on the ingestion run, not the queue receipt |\n| 4 | Clean | Tables | `oxygen tables dedupe <accounts> --on domain --normalize domain --json` → `--apply --approved --merge-values fill-empty`; `oxygen tables auto-dedupe set <accounts> --on domain --normalize domain --json`; `oxygen companies enrich preview <accounts> --missing-fields domain,linkedin_url,headcount,industry --json` → `oxygen companies enrich run <accounts> --mode live --max-credits <cap> --approved --json` | `domain`; rows with firmographic gaps | `--scan-limit` 50,000 / 200,000 cap; enrichment scoped by its preview | dedupe apply: free internal delete, losers kept in row history. `fill-firmographics`: paid, human | dedupe preview reruns to 0 groups; `oxygen cells inspect <accounts> <row_id> domain --json` carries provider and cost | groups that are not one company; a provider in the preview reading blocked or benched |\n| 5 | Score | Tables — kit stage `icp-fit-scoring` | `oxygen columns run <accounts> icp_fit_score --limit 10 --dry-run --json` → `--limit 25 --background --approved --max-credits <cap> --json` | the `icp` page (auto-prepended), `company_name`, `domain` | the sample only; `--limit` defaults to 10 | `score-accounts`: paid, human for the cap | `oxygen cells inspect <accounts> <row_id> icp_fit_score --json` — one high, one medium, one low, each `evidence` citing a checkable fact | bands spread evenly, or evidence reads generic: fix the ICP page, not the prompt |\n| 6 | Widen | Tables — company search, again | re-plan the surviving segment, then the stage-3 live command with the new plan and `--max-pages <n>` | segments whose sample cleared the widen bar | one segment per widen; auto-pagination stops at the cap | `widen-coverage`: paid, human, **per segment** | the `auto_paged` block; `oxygen lead-sourcing audit <accounts> --spec ./icp-spec.json --json` | a segment whose sample landed mostly `low` — more pages of a bad filter is more junk |\n| 7 | Personas | Tables — Blueprint `contact-finding` | `oxygen blueprints apply contact-finding --json` → `oxygen people search plan --company-domains <accepted-domains> --titles \"<persona>\" --title-match exact --max-per-company <n> --json` → `oxygen people search run --plan-json <plan> --route-id <route_id> --table <contacts> --upsert-key linkedin_url --mode live --max-credits <cap> --approved --json` | the accepted-domain list from the gate below; one persona title set | `--max-per-company <n>`; upsert dedupes on `linkedin_url` | `source-personas`: paid, human | `oxygen tables query <contacts> --limit 25 --json`; `oxygen tables link <contacts> --to <accounts> --on company_domain --json` → `--approved` | a route whose `tool_access` is not `runnable`; any route that drops `company.domains` |\n| 8 | Hand off and learn | Sequences / Records / Knowledge Graph | `oxygen recipes show outbound-pilot-50 --json`; `oxygen tables promote <accounts> --object companies --dry-run --json` → `--approved`; `oxygen knowledge log append --event decision --slug account-map --summary \"...\" --json` | accepted accounts, their contacts, the run receipts | the receiving motion's caps; one rule changed per loop | `outbound-enrollment` belongs to that motion, never this one; canonical pages route through the proposal queue | `oxygen crm pipeline --json` after the first warm lead; `oxygen knowledge lint --json` | enrolling the contacts table wholesale — sourcing never initiates outreach |\n\n## Numeric guardrails\n\n- **Sample: 25 accounts, one page** — the recipe's `pilot.size`; `--max-pages 1` stops auto-pagination widening a filter nobody has read.\n- **Plan ceiling: 50,000** per company or people plan (`--target-count`); above it the plan returns a clamp warning and segmentation guidance. Segment instead of raising it.\n- **Bands.** `icp_fit_score` returns `band` (high / medium / low), a 0-100 `score` and one `evidence` sentence; borderline accounts land `medium`, never a generous `high`.\n- **Widen bar.** Widen only when a segment's sample clears the bar you wrote down; the recipe's default is high + medium above 50%. Plausible-fit on page 1 runs 60-80% for a sharp ICP, under ~40% means loose filters — both carry `benchmark_basis: estimate`, so they calibrate, they do not measure.\n- **Scope defaults.** Dedupe `--scan-limit` is 50,000 rows (cap 200,000), and standing auto-dedupe runs **before** auto-run so enrichment is never queued for rows about to be deleted. `columns run --limit` defaults to 10, inline deterministic runs cap at 25, `--all` requires `--background`. Personas per account have no default — set `--max-per-company` deliberately. Capacity: 3M rows per Table, 25M per workspace (`oxygen limits show --json`).\n- **Every `--max-credits <cap>` comes from the immediately preceding preview**, route estimate or `recommended_max_credits`. A too-low cap on a waterfall does not stop the run: the expensive lane is refused on its own and the cascade advances, so a cheaper lane still bills while the good one is skipped. Premium managed lanes stay off unless `--allow-premium-lanes` is passed; read a lane's price from `oxygen tools get <tool_id> --json`, never from prose.\n- **`domain` is the account identity, `linkedin_url` the person identity** — both are the upsert keys, neither is overwritten, and provenance accumulates in `source`.\n\n## Accepted-account gate spec\n\nAn account reaches persona sourcing only when `icp_fit_score.band` is `high`, or `medium` **with** an evidence string naming a verifiable fact; `score` is at or above the band floor written into the account-map page; `domain` is present, canonical, and not a directory, aggregator or search URL; the row is not excluded by the ICP spec (competitor, current customer, wrong geography, wrong size); and the row survived dedupe.\n\nEnforcement is a formula column, not prose. Validate it free, attach it, then materialise it — formula values compute when read, so a filter on one is refused by default:\n\n```bash\noxygen formulas validate <accounts> --expression 'if(and(or(path(icp_fit_score, \"band\") == \"high\", path(icp_fit_score, \"band\") == \"medium\"), is_blank(excluded_reason)), \"accepted\", \"held\")' --rows 5 --json\noxygen columns add <accounts> --kind formula --key account_accepted --label \"Accepted\" --definition-json '<validated expression>' --json\noxygen columns run <accounts> account_accepted --force --json\noxygen tables query <accounts> --filter-json '{\"column\":\"account_accepted\",\"op\":\"eq\",\"value\":\"accepted\"}' --formula-values materialized --fields domain --limit 1000 --json\noxygen tables views create <accounts> --name \"Accepted accounts\" --json\n```\n\nNothing bypasses this: never hand the whole accounts table to `oxygen people search run`, never source people at an account whose `icp_fit_score` cell is empty or errored, and never promote a `held` row because the market looks thin. A thin accepted set is a filter or ICP problem, fixed upstream.\n\n## Approval gates, mapped to who decides\n\n| Gate | What it authorises | Attended Copilot | Unattended run |\n| --- | --- | --- | --- |\n| `kit-apply` | 0-credit install of the accounts table and scoring column | auto-approvable workspace write, one card | installs only; arms nothing |\n| `source-sample` | one live company-search page under a cap | human card — provider spend | its own approved per-delivery ceiling |\n| dedupe `--apply --approved` | deleting duplicates (recoverable from row history) | auto-approvable workspace write | standing auto-dedupe, no per-write approval |\n| `fill-firmographics` | one capped company-enrichment run | human card | its own ceiling |\n| `score-accounts` | one paid AI column run under a cap | human card for the cap | its own ceiling; `tables auto-run set` is scoped standing permission for the listed columns |\n| `widen-coverage` | one more segment, re-granted every time | human card, per segment | never standing — a widen is a new purchase |\n| `source-personas` | one live people-search run scoped to accepted domains | human card | its own ceiling |\n| `tables promote --approved` | writing table columns onto matched CRM records | auto-approvable internal truth write | free, still explicit |\n| `outbound-enrollment` | contacting anyone at all | owned by the outbound motion | never granted here |\n\nReads — `recipes show`, both `search plan` commands, `tables query`, `cells inspect`, `lead-sourcing audit`, `budget list`, `limits show` — need no approval and spend nothing. Canonical wiki edits file a proposal a human approves.\n\n## Failure modes\n\n- **Free-text keyword where the provider wants an enum** → `invalid_provider_enum_value`, or worse a noisy page. Fetch the catalog and pass the exact string; free text only tightens an already enum-grounded segment.\n- **A dropped filter read as applied, or an invented market size.** `filter_application[]` reports applied vs dropped per route with the reason; quote `estimated_match_count` only when its `basis` is `provider_count` — any other basis is derived from your requested target, not from the market.\n- **Scoring the whole table before reading page 1** — the failure this motion exists to prevent: it spends several times over before anyone knows the filters work.\n- **Filtering a formula column.** `tables query` refuses formula filters by default because displayed formulas evaluate live. Run with `--force` (free), then `--formula-values materialized`.\n- **Appending instead of merging.** A source run without `--upsert-key domain` re-adds the same companies every page; collapse them, then arm the standing auto-dedupe. Enriching before deduping pays twice for one company.\n- **A benched or unknown managed provider.** `provider_availability` carries `degraded: true` or `availability: \"unknown\"`, and a live run refuses a benched managed primary before table creation. Read each entry's `next_action` — funding clears neither a staff hold nor a rejected key, and an unknown snapshot is re-previewed, not assumed.\n- **A preview-only people route treated as runnable.** Apollo and ContactOut people search return masked records without the stable `linkedin_url` the Contacts upsert contract needs, so `recommended_live_route_id` can name the right contract without naming a runnable route. Confirm `tool_access` is `runnable`, and never accept a route that drops `company.domains` — that is the gate leaking.\n- **An AI column asked a web question.** AI columns have no web access and answer from the row; web answers belong in a `--kind research` column with a `--research-query`. And a `queued` / `not_started` run has captured nothing yet — `oxygen table-runs cancel <run_id> --json` before pickup leaves spend at 0.\n\n## Data-quality checks (after every sourcing cycle)\n\n1. `oxygen tables query <accounts> --limit 100 --json` — rows with a blank `domain`, or one that is a directory, aggregator or search URL. Clear those cells **and** the row's `icp_fit_score` before re-scoring; poisoned cells are not re-scored on their own.\n2. `oxygen tables dedupe <accounts> --on domain --normalize domain --json` — zero groups once auto-dedupe is armed; anything else means a write path bypassed the key.\n3. `oxygen lead-sourcing audit <accounts> --spec ./icp-spec.json --json` — a reason on every exclusion; an exclusion with no reason is a filter you cannot defend.\n4. `oxygen cells inspect <accounts> <row_id> icp_fit_score --json` on one high, one medium and one low row; evidence citing nothing checkable means a thin ICP page.\n5. `oxygen tables query <contacts> --limit 100 --json` — contacts per accepted account; zero is a persona-filter problem, not a market problem. `oxygen table-runs provider-summary <run_id> --json` says which provider answered and at what cost per row.\n\n## Learning loop\n\n- **What auto-files:** the ingestion run and its item outputs, per-cell provider and cost provenance, the knowledge log line each kit apply writes, `table-runs provider-summary`, and the audit output.\n- **What to synthesise monthly:** high-fit share by segment; contacts per accepted account; credits per *accepted account* (not per sourced row — the denominator that matters is the account someone would work); and, once the outbound companion has run, reply rate by segment joined back to the map.\n- **What to change, one rule per loop:** one filter, one band floor, one excluded segment, or one persona title set. Write the change and its reason into the account-map page; canonical positioning files a proposal instead.\n- **\"Good\", written before the first live run:** an accepted account is one with a reachable buyer persona in a segment where you can name a reason you win. Coverage without that definition is a row count, not a market map.\n\n## Go-live checklist\n\n1. `oxygen knowledge page get icp --json` and `oxygen knowledge page get offers --json` — both `active`, the ICP sentence carrying an explicit EXCLUDE clause. (`oxygen knowledge status --json` describes the local CLI mirror and can report zero pages while the workspace is filled; it is not the readiness check.)\n2. `oxygen integrations list --json` and `oxygen billing balance --json` — a company-search provider reachable, headroom for the sample.\n3. `oxygen tools enums get blitzapi industry --query <segment> --json`, then `oxygen companies search plan --prompt \"<ICP sentence>\" --filters-json '<typed filters>' --estimate --json`; read `filter_application` and `provider_availability`, save the plan to a file.\n4. `oxygen recipes apply icp-to-account-map --dry-run --json`, then apply — table and `icp_fit_score` installed, nothing armed.\n5. `source-sample` granted; `oxygen table-ingestions wait <ingestion_run_id> --json` green; 25 rows readable.\n6. Dedupe applied and `oxygen tables auto-dedupe set <accounts> --on domain --normalize domain --json` armed; `fill-firmographics` and `score-accounts` granted; one high, one medium and one low read by hand.\n7. `account_accepted` validated, attached, materialised; `oxygen tables views create <accounts> --name \"Accepted accounts\" --json`.\n8. Widen bar written down; `widen-coverage` granted for the first segment only.\n9. `oxygen blueprints apply contact-finding --json`; `source-personas` for one persona set at accepted domains; `oxygen tables link <contacts> --to <accounts> --on company_domain --json` previewed, then `--approved`.\n10. `oxygen budget list --json` and `oxygen limits show --json` — org backstop and capacity headroom in place; `oxygen knowledge page upsert --slug account-map --type research_note --json` records segments, filters, band floor and widen decisions.\n\n## Open questions (state them, do not resolve them)\n\n- Company search publishes no `schedulable` field the way signal search does, so a recurring refresh is discovered by attempting `oxygen feeds bind <accounts> --kind company_search --upsert-key domain --every daily@9 --max-credits <cap> --approved --json` and treating a `feed_not_incremental` refusal as the answer. Until then the supported cadence is a monthly re-plan through the same gates.\n- The recipe's fit bands are `benchmark_basis: estimate` with no workspace-measured baseline behind them, and the canonical ICP scorer function is not yet ratifiable — this playbook uses the kit's table AI column, never a callable.\n- Whether accepted accounts should become CRM company records at map time is unratified. This playbook keeps the map in Tables and promotes only accounts a human is working; a CRM *person* is created at the warm-lead gate downstream, never at sourcing time.\n- Cross-table suppression is read-only through `oxygen tables dedupe <accounts> --on domain --against <suppression-table> --against-column domain --json`; no standing suppression policy exists on a source run, so exclusions live in the plan's EXCLUDE clause and the audit spec.\n\n## Resume point\n\n`oxygen recipes show icp-to-account-map --json`. Its `kit` block names each stage's install status; `oxygen tables list --json` says whether an accounts or contacts table already exists, and `oxygen tables auto-dedupe get <accounts> --json` plus `oxygen tables auto-run get <accounts> --json` say what already runs on writes. Start there, never from scratch.\n\n## What the human still has to do\n\nNothing above contacts anyone, and nothing widens on its own. The human writes the ICP sentence and band floor into `oxygen knowledge page upsert --slug account-map --json`; grants `source-sample`, `fill-firmographics` and `score-accounts`, each with a cap read from its own preview; reads one high, one medium and one low row with `oxygen cells inspect <accounts> <row_id> icp_fit_score --json` and decides whether the filters or the ICP page need fixing; grants `widen-coverage` once per segment; grants `source-personas` after choosing the persona titles and `--max-per-company`; and decides whether the accepted map goes to `outbound-pilot-50` now or after another coverage pass. That read is the step routinely skipped, and the one the whole motion is built around.\n";
|
|
11
|
+
readonly sha256: "f3743642d6b0a1f91d66ea674ea56c3479e34ee840c89f043a2bbfc94dd6efb6";
|
|
12
|
+
readonly content: "---\nname: tam-sourcing\ndescription: \"Turn the workspace ICP into a sourced, scored, deduplicated account map and the buyer personas at those accounts — a bounded sample first, coverage widened only on segments the sample proved — then hand it to outbound. Load it when the whole market map is the ask.\"\n---\n\n# Playbook: TAM sourcing\n\n## Motion in one sentence\n\nThe ICP compiles into a provider-grounded sourcing plan, one bounded sample of accounts is sourced live, deduplicated and filled, every account is scored against that same ICP with its evidence on the row, the sample's band distribution decides which segments deserve coverage, coverage widens one segment at a time through the same gate, buyer personas are sourced only at accounts that passed it, and the accepted map is handed to an outbound motion. The sample before the coverage is the play; the order is companies → fit → people, never people first.\n\nRecipe and kit: `oxygen recipes show icp-to-account-map --json` (its `journey.slug` is `tam-sourcing`); the companion it hands to is `oxygen recipes show outbound-pilot-50 --json`. Read both first — `recipes show` reports which stages this workspace already has.\n\n## Stage table\n\n| # | Stage | Owner | Install / command | Inputs | Cap | Approval | Verify | Stop |\n| --- | --- | --- | --- | --- | --- | --- | --- | --- |\n| 0 | Context | Knowledge Graph | `oxygen knowledge page get icp --json`; `oxygen knowledge page get offers --json` (`oxygen knowledge status` reports the local CLI mirror, not the workspace) | `company`, `offering`, `icp` pages | — | none | pages `active`, not seed stubs; the ICP names segment, geography, size **and who is out** | a stub ICP, or no exclusion clause |\n| 1 | Plan | Tables — company-search planner | `oxygen tools enums get blitzapi industry --query <segment> --json` → `oxygen companies search plan --prompt \"<ICP sentence with EXCLUDE clause>\" --filters-json '<typed filters>' --estimate --json` | one ICP sentence; exact enum strings | `--target-count` ceiling 50,000 per plan | none — no provider call, no spend | `filter_application[].dropped`, `provider_availability`, `estimated_match_count.basis`, `recommended_live_route_id` | a filter you need lands in `dropped_constraints`; a managed primary reads `degraded: true` |\n| 2 | Scaffold | Tables — kit stage `account-sourcing` | `oxygen recipes apply icp-to-account-map --dry-run --json` → the same without `--dry-run` | — | 0 credits, no provider call, no external write | `kit-apply`: human, once for the kit | `oxygen tables describe <accounts> --json` shows `company_name`, `domain`, `company_linkedin_url`, `source`, `fit_notes` | a stage already installed — never re-install it |\n| 3 | Source the sample | Tables — company search | `oxygen companies search run --plan-json <plan> --route-id <route_id> --table <accounts> --upsert-key domain --mode dry_run --json` → `--mode live --max-pages 1 --max-credits <cap> --approved` | the plan you actually read | one page, the recipe's 25-account pilot; `<cap>` from the route estimate | `source-sample`: paid, human, server-enforced | `oxygen table-ingestions wait <ingestion_run_id> --json`; `oxygen tables query <accounts> --limit 25 --json` | `spend_cap_too_low`; near-zero rows; `data_status` still `pending` — the verdict is on the ingestion run, not the queue receipt |\n| 4 | Clean | Tables | `oxygen tables dedupe <accounts> --on domain --normalize domain --json` → `--apply --approved --merge-values fill-empty`; `oxygen tables auto-dedupe set <accounts> --on domain --normalize domain --json`; `oxygen companies enrich preview <accounts> --missing-fields domain,linkedin_url,headcount,industry --json` → `oxygen companies enrich run <accounts> --mode live --max-credits <cap> --approved --json` | `domain`; rows with firmographic gaps | `--scan-limit` 50,000 / 200,000 cap; enrichment scoped by its preview | dedupe apply: free internal delete, losers kept in row history. `fill-firmographics`: paid, human | dedupe preview reruns to 0 groups; `oxygen cells inspect <accounts> <row_id> domain --json` carries provider and cost | groups that are not one company; a provider in the preview reading blocked or benched |\n| 5 | Score | Tables — kit stage `icp-fit-scoring` | `oxygen columns run <accounts> icp_fit_score --limit 10 --dry-run --json` → `--limit 25 --background --approved --max-credits <cap> --json` | the `icp` page (auto-prepended), `company_name`, `domain` | the sample only; `--limit` defaults to 10 | `score-accounts`: paid, human for the cap | `oxygen cells inspect <accounts> <row_id> icp_fit_score --json` — one high, one medium, one low, each `evidence` citing a checkable fact | bands spread evenly, or evidence reads generic: fix the ICP page, not the prompt |\n| 6 | Widen | Tables — company search, again | re-plan the surviving segment, then the stage-3 live command with the new plan and `--max-pages <n>` | segments whose sample cleared the widen bar | one segment per widen; auto-pagination stops at the cap | `widen-coverage`: paid, human, **per segment** | the `auto_paged` block; `oxygen lead-sourcing audit <accounts> --spec ./icp-spec.json --json` | a segment whose sample landed mostly `low` — more pages of a bad filter is more junk |\n| 7 | Personas | Tables — Blueprint `contact-finding` | `oxygen blueprints apply contact-finding --json` → `oxygen people search plan --company-domains <accepted-domains> --titles \"<persona>\" --title-match exact --max-per-company <n> --json` → `oxygen people search run --plan-json <plan> --route-id <route_id> --table <contacts> --upsert-key linkedin_url --mode live --max-credits <cap> --approved --json` | the accepted-domain list from the gate below; one persona title set | `--max-per-company <n>`; upsert dedupes on `linkedin_url` | `source-personas`: paid, human | `oxygen tables query <contacts> --limit 25 --json`; `oxygen tables link <contacts> --to <accounts> --on company_domain --json` → `--approved` | a route whose `tool_access` is not `runnable`; any route that drops `company.domains` |\n| 8 | Hand off and learn | Sequences / Records / Knowledge Graph | `oxygen recipes show outbound-pilot-50 --json`; `oxygen tables promote <accounts> --object companies --dry-run --json` → `--approved`; `oxygen knowledge log append --event decision --slug account-map --summary \"...\" --json` | accepted accounts, their contacts, the run receipts | the receiving motion's caps; one rule changed per loop | `outbound-enrollment` belongs to that motion, never this one; canonical pages route through the proposal queue | `oxygen crm pipeline --json` after the first warm lead; `oxygen knowledge lint --json` | enrolling the contacts table wholesale — sourcing never initiates outreach |\n\n## Numeric guardrails\n\n- **Sample: 25 accounts, one page** — the recipe's `pilot.size`; `--max-pages 1` stops auto-pagination widening a filter nobody has read.\n- **Plan ceiling: 50,000** per company or people plan (`--target-count`); above it the plan returns a clamp warning and segmentation guidance. Segment instead of raising it.\n- **Bands.** `icp_fit_score` returns `band` (high / medium / low), a 0-100 `score` and one `evidence` sentence; borderline accounts land `medium`, never a generous `high`.\n- **Widen bar.** Widen only when a segment's sample clears the bar you wrote down; the recipe's default is high + medium above 50%. Plausible-fit on page 1 runs 60-80% for a sharp ICP, under ~40% means loose filters — both carry `benchmark_basis: estimate`, so they calibrate, they do not measure.\n- **Scope defaults.** Dedupe `--scan-limit` is 50,000 rows (cap 200,000), and standing auto-dedupe runs **before** auto-run so enrichment is never queued for rows about to be deleted. `columns run --limit` defaults to 10, inline deterministic runs cap at 25, `--all` requires `--background`. Personas per account have no default — set `--max-per-company` deliberately. Capacity follows the plan size: rows per Table and per workspace are in `oxygen limits show --json` (`storage_capacity`); at a limit only new rows are refused.\n- **Every `--max-credits <cap>` comes from the immediately preceding preview**, route estimate or `recommended_max_credits`. A too-low cap on a waterfall does not stop the run: the expensive lane is refused on its own and the cascade advances, so a cheaper lane still bills while the good one is skipped. Premium managed lanes stay off unless `--allow-premium-lanes` is passed; read a lane's price from `oxygen tools get <tool_id> --json`, never from prose.\n- **`domain` is the account identity, `linkedin_url` the person identity** — both are the upsert keys, neither is overwritten, and provenance accumulates in `source`.\n\n## Accepted-account gate spec\n\nAn account reaches persona sourcing only when `icp_fit_score.band` is `high`, or `medium` **with** an evidence string naming a verifiable fact; `score` is at or above the band floor written into the account-map page; `domain` is present, canonical, and not a directory, aggregator or search URL; the row is not excluded by the ICP spec (competitor, current customer, wrong geography, wrong size); and the row survived dedupe.\n\nEnforcement is a formula column, not prose. Validate it free, attach it, then materialise it — formula values compute when read, so a filter on one is refused by default:\n\n```bash\noxygen formulas validate <accounts> --expression 'if(and(or(path(icp_fit_score, \"band\") == \"high\", path(icp_fit_score, \"band\") == \"medium\"), is_blank(excluded_reason)), \"accepted\", \"held\")' --rows 5 --json\noxygen columns add <accounts> --kind formula --key account_accepted --label \"Accepted\" --definition-json '<validated expression>' --json\noxygen columns run <accounts> account_accepted --force --json\noxygen tables query <accounts> --filter-json '{\"column\":\"account_accepted\",\"op\":\"eq\",\"value\":\"accepted\"}' --formula-values materialized --fields domain --limit 1000 --json\noxygen tables views create <accounts> --name \"Accepted accounts\" --json\n```\n\nNothing bypasses this: never hand the whole accounts table to `oxygen people search run`, never source people at an account whose `icp_fit_score` cell is empty or errored, and never promote a `held` row because the market looks thin. A thin accepted set is a filter or ICP problem, fixed upstream.\n\n## Approval gates, mapped to who decides\n\n| Gate | What it authorises | Attended Copilot | Unattended run |\n| --- | --- | --- | --- |\n| `kit-apply` | 0-credit install of the accounts table and scoring column | auto-approvable workspace write, one card | installs only; arms nothing |\n| `source-sample` | one live company-search page under a cap | human card — provider spend | its own approved per-delivery ceiling |\n| dedupe `--apply --approved` | deleting duplicates (recoverable from row history) | auto-approvable workspace write | standing auto-dedupe, no per-write approval |\n| `fill-firmographics` | one capped company-enrichment run | human card | its own ceiling |\n| `score-accounts` | one paid AI column run under a cap | human card for the cap | its own ceiling; `tables auto-run set` is scoped standing permission for the listed columns |\n| `widen-coverage` | one more segment, re-granted every time | human card, per segment | never standing — a widen is a new purchase |\n| `source-personas` | one live people-search run scoped to accepted domains | human card | its own ceiling |\n| `tables promote --approved` | writing table columns onto matched CRM records | auto-approvable internal truth write | free, still explicit |\n| `outbound-enrollment` | contacting anyone at all | owned by the outbound motion | never granted here |\n\nReads — `recipes show`, both `search plan` commands, `tables query`, `cells inspect`, `lead-sourcing audit`, `budget list`, `limits show` — need no approval and spend nothing. Canonical wiki edits file a proposal a human approves.\n\n## Failure modes\n\n- **Free-text keyword where the provider wants an enum** → `invalid_provider_enum_value`, or worse a noisy page. Fetch the catalog and pass the exact string; free text only tightens an already enum-grounded segment.\n- **A dropped filter read as applied, or an invented market size.** `filter_application[]` reports applied vs dropped per route with the reason; quote `estimated_match_count` only when its `basis` is `provider_count` — any other basis is derived from your requested target, not from the market.\n- **Scoring the whole table before reading page 1** — the failure this motion exists to prevent: it spends several times over before anyone knows the filters work.\n- **Filtering a formula column.** `tables query` refuses formula filters by default because displayed formulas evaluate live. Run with `--force` (free), then `--formula-values materialized`.\n- **Appending instead of merging.** A source run without `--upsert-key domain` re-adds the same companies every page; collapse them, then arm the standing auto-dedupe. Enriching before deduping pays twice for one company.\n- **A benched or unknown managed provider.** `provider_availability` carries `degraded: true` or `availability: \"unknown\"`, and a live run refuses a benched managed primary before table creation. Read each entry's `next_action` — funding clears neither a staff hold nor a rejected key, and an unknown snapshot is re-previewed, not assumed.\n- **A preview-only people route treated as runnable.** Apollo and ContactOut people search return masked records without the stable `linkedin_url` the Contacts upsert contract needs, so `recommended_live_route_id` can name the right contract without naming a runnable route. Confirm `tool_access` is `runnable`, and never accept a route that drops `company.domains` — that is the gate leaking.\n- **An AI column asked a web question.** AI columns have no web access and answer from the row; web answers belong in a `--kind research` column with a `--research-query`. And a `queued` / `not_started` run has captured nothing yet — `oxygen table-runs cancel <run_id> --json` before pickup leaves spend at 0.\n\n## Data-quality checks (after every sourcing cycle)\n\n1. `oxygen tables query <accounts> --limit 100 --json` — rows with a blank `domain`, or one that is a directory, aggregator or search URL. Clear those cells **and** the row's `icp_fit_score` before re-scoring; poisoned cells are not re-scored on their own.\n2. `oxygen tables dedupe <accounts> --on domain --normalize domain --json` — zero groups once auto-dedupe is armed; anything else means a write path bypassed the key.\n3. `oxygen lead-sourcing audit <accounts> --spec ./icp-spec.json --json` — a reason on every exclusion; an exclusion with no reason is a filter you cannot defend.\n4. `oxygen cells inspect <accounts> <row_id> icp_fit_score --json` on one high, one medium and one low row; evidence citing nothing checkable means a thin ICP page.\n5. `oxygen tables query <contacts> --limit 100 --json` — contacts per accepted account; zero is a persona-filter problem, not a market problem. `oxygen table-runs provider-summary <run_id> --json` says which provider answered and at what cost per row.\n\n## Learning loop\n\n- **What auto-files:** the ingestion run and its item outputs, per-cell provider and cost provenance, the knowledge log line each kit apply writes, `table-runs provider-summary`, and the audit output.\n- **What to synthesise monthly:** high-fit share by segment; contacts per accepted account; credits per *accepted account* (not per sourced row — the denominator that matters is the account someone would work); and, once the outbound companion has run, reply rate by segment joined back to the map.\n- **What to change, one rule per loop:** one filter, one band floor, one excluded segment, or one persona title set. Write the change and its reason into the account-map page; canonical positioning files a proposal instead.\n- **\"Good\", written before the first live run:** an accepted account is one with a reachable buyer persona in a segment where you can name a reason you win. Coverage without that definition is a row count, not a market map.\n\n## Go-live checklist\n\n1. `oxygen knowledge page get icp --json` and `oxygen knowledge page get offers --json` — both `active`, the ICP sentence carrying an explicit EXCLUDE clause. (`oxygen knowledge status --json` describes the local CLI mirror and can report zero pages while the workspace is filled; it is not the readiness check.)\n2. `oxygen integrations list --json` and `oxygen billing balance --json` — a company-search provider reachable, headroom for the sample.\n3. `oxygen tools enums get blitzapi industry --query <segment> --json`, then `oxygen companies search plan --prompt \"<ICP sentence>\" --filters-json '<typed filters>' --estimate --json`; read `filter_application` and `provider_availability`, save the plan to a file.\n4. `oxygen recipes apply icp-to-account-map --dry-run --json`, then apply — table and `icp_fit_score` installed, nothing armed.\n5. `source-sample` granted; `oxygen table-ingestions wait <ingestion_run_id> --json` green; 25 rows readable.\n6. Dedupe applied and `oxygen tables auto-dedupe set <accounts> --on domain --normalize domain --json` armed; `fill-firmographics` and `score-accounts` granted; one high, one medium and one low read by hand.\n7. `account_accepted` validated, attached, materialised; `oxygen tables views create <accounts> --name \"Accepted accounts\" --json`.\n8. Widen bar written down; `widen-coverage` granted for the first segment only.\n9. `oxygen blueprints apply contact-finding --json`; `source-personas` for one persona set at accepted domains; `oxygen tables link <contacts> --to <accounts> --on company_domain --json` previewed, then `--approved`.\n10. `oxygen budget list --json` and `oxygen limits show --json` — org backstop and capacity headroom in place; `oxygen knowledge page upsert --slug account-map --type research_note --json` records segments, filters, band floor and widen decisions.\n\n## Open questions (state them, do not resolve them)\n\n- Company search publishes no `schedulable` field the way signal search does, so a recurring refresh is discovered by attempting `oxygen feeds bind <accounts> --kind company_search --upsert-key domain --every daily@9 --max-credits <cap> --approved --json` and treating a `feed_not_incremental` refusal as the answer. Until then the supported cadence is a monthly re-plan through the same gates.\n- The recipe's fit bands are `benchmark_basis: estimate` with no workspace-measured baseline behind them, and the canonical ICP scorer function is not yet ratifiable — this playbook uses the kit's table AI column, never a callable.\n- Whether accepted accounts should become CRM company records at map time is unratified. This playbook keeps the map in Tables and promotes only accounts a human is working; a CRM *person* is created at the warm-lead gate downstream, never at sourcing time.\n- Cross-table suppression is read-only through `oxygen tables dedupe <accounts> --on domain --against <suppression-table> --against-column domain --json`; no standing suppression policy exists on a source run, so exclusions live in the plan's EXCLUDE clause and the audit spec.\n\n## Resume point\n\n`oxygen recipes show icp-to-account-map --json`. Its `kit` block names each stage's install status; `oxygen tables list --json` says whether an accounts or contacts table already exists, and `oxygen tables auto-dedupe get <accounts> --json` plus `oxygen tables auto-run get <accounts> --json` say what already runs on writes. Start there, never from scratch.\n\n## What the human still has to do\n\nNothing above contacts anyone, and nothing widens on its own. The human writes the ICP sentence and band floor into `oxygen knowledge page upsert --slug account-map --json`; grants `source-sample`, `fill-firmographics` and `score-accounts`, each with a cap read from its own preview; reads one high, one medium and one low row with `oxygen cells inspect <accounts> <row_id> icp_fit_score --json` and decides whether the filters or the ICP page need fixing; grants `widen-coverage` once per segment; grants `source-personas` after choosing the persona titles and `--max-per-company`; and decides whether the accepted map goes to `outbound-pilot-50` now or after another coverage pass. That read is the step routinely skipped, and the one the whole motion is built around.\n";
|
|
13
13
|
}, {
|
|
14
14
|
readonly slug: "linkedin-content-strategy";
|
|
15
15
|
readonly title: "Playbook: LinkedIn content strategy";
|
|
16
16
|
readonly sources: readonly ["oxygen-playbooks/playbooks/linkedin-content-strategy.md"];
|
|
17
|
-
readonly sha256: "
|
|
18
|
-
readonly content: "---\nname: linkedin-content-strategy\ndescription: \"Install and run the founder LinkedIn content engine: a wiki-grounded strategy page, an idea backlog, a week of drafts approved post by post, scheduled publishing, answered comments, weekly engagement read-back, outlier mining, and the hand-off of engagers to inbound-led outbound.\"\n---\n\n# Playbook: LinkedIn content strategy\n\n## Motion in one sentence\n\nA strategy page written from the wiki fixes the pillars, cadence and voice; ideas accumulate in a backlog; a week of drafts is generated from that wiki and approved one post at a time; the scheduler publishes each from the founder's own account; comments are answered through a previewed reply; engagement is read back weekly; outlier mining says which structures travelled; the lessons file back into the wiki; and the engagers become the inbound signal. Each stage reads the page the last one wrote — a posting habit without the strategy page is a treadmill, and an outlier bank without the habit is a swipe file nobody drains.\n\nRead first: `oxygen recipes show founder-posting-system --json`, `oxygen recipes show weekly-content-calendar --json`, `oxygen recipes show content-outlier-mining --json` (the stage 7 kit). Mechanics: `oxygen skills get oxygen-linkedin-marketing --json`.\n\n## Stage table\n\n| # | Stage | Owner | Install / command | Inputs | Cap | Approval | Verify | Stop |\n| --- | --- | --- | --- | --- | --- | --- | --- | --- |\n| 0 | Context + strategy | Knowledge Graph | `oxygen knowledge resolve --purpose outbound_copy --require-ready --json` → `oxygen knowledge page upsert --slug content-strategy --type strategy --title \"Content strategy\" --status active --body \"<pillars / cadence / voice>\" --json` | `positioning`, `icp`, `offers`, `voice`, all `active` | 3–5 pillars, each naming one ICP problem | none — a `strategy` page is working knowledge, logged as a revision | `oxygen knowledge page get content-strategy --json` | a stub ICP page, or a pillar with no buyer problem |\n| 1 | Sender | Publishing | `oxygen senders list --status active --json`; `oxygen senders health <sender_id> --json` (`oxygen senders limits get` for the budgets) | one connected LinkedIn account | own-feed ceiling 25 posts / 24h | none — reads | `active`, no open checkpoint | `restricted` / `credentials_required`: reconnect, never route around it |\n| 2 | Backlog | Publishing | `oxygen publishing ideas add --text \"<angle>\" --topic <pillar> --json`; `oxygen publishing ideas list --json` | angles as they occur; structures from stage 7 | two weeks of slots ahead | none (internal write) | `oxygen publishing ideas list --limit 50 --json` | an empty backlog on drafting day: mine outliers, don't draft from taste |\n| 3 | Draft + queue | Publishing | `oxygen publishing posts draft --template personal_story --topic \"<angle>\" --max-credits <cap> --json` → `oxygen publishing drafts edit <draft_id> --text-file mon.txt --json` → `oxygen publishing drafts accept <draft_id> --publish-at <iso+offset> --sender <sender_id> --json` | a pillar + a backlog angle per slot | 3–5 posts/week; 1–5 variants (default 3) | `draft-week`: paid, cap from the preview; queueing needs none — it never sends | `oxygen publishing posts list --approval-status needs_approval --json` | a bare `--publish-at`: with no offset it is stored as UTC |\n| 4 | Review + publish | Publishing | `oxygen publishing posts review <post_id> --max-credits <cap> --json`; `oxygen publishing mentions resolve --text-file mon.txt --json` → `oxygen publishing posts approve <post_id> --json` | the exact final text, read by the founder | ≤15% promotion posts; ≤1 lead-magnet CTA/week | `review-post`: paid, advisory. `post-publish`: human, every post, never batched | `oxygen publishing posts get <post_id> --json` — attempts, provider id, deep-link | an unresolved `@<public-identifier>`; `linkedin_rate_limited` |\n| 5 | Comments | Publishing | `oxygen publishing comments list --view unanswered --json` → `oxygen publishing comments reply <comment_id> --text-file reply.txt --json` → `oxygen publishing comments approve <action_id> --content-hash <sha256> --approved --json` → `oxygen publishing comments resolve <comment_id> --json` | the unanswered queue (30-day scope) | one reply per comment, inside 48h | `comment-reply`: human approves that exact previewed text and its hash | the queue drains; handled items read `resolved` | a comment older than 48h with no reply |\n| 6 | Read back | Publishing + Posts | `oxygen publishing analytics summary --channel linkedin --range 30d --json` (winners, `leader` lead, `null_reasons`); `oxygen publishing analytics timeseries --channel linkedin --range 30d --json` (earned per day, growing or shrinking vs the previous period, follower change); `oxygen publishing analytics post <post_id> --json`; `oxygen posts get --post <post_url> --json` | published posts | metrics refresh ~30 days after publish | none — reads | reactions, comments, reshares and — on your own original posts — impressions present; reposts counted in `reposts_excluded`, not in totals | every metric zero after two days: confirm `published`, not `deferred` |\n| 7 | Mine outliers | Workflows — kit stage `linkedin-keyword-outliers` | `oxygen recipes apply content-outlier-mining --dry-run --json` → without `--dry-run`; then `oxygen blueprints apply linkedin-creator-outliers --table-ref content_outliers=<bank_table_id> --json` | 3–4 buyer-language keywords; ≤25 public `/in/` URLs | 7-day lookback; 1 page/keyword on the pilot; ceiling from preflight | `pilot-live` then `sweep-arm`: human, re-granted on reapply | `oxygen workflows tail <run_id> --json`; `oxygen tables query <bank_table_id> --json` | everything near par, or top rows are hiring posts |\n| 8 | Learn | Knowledge Graph | `oxygen knowledge synthesize --kind voice --json` → `--approved`; `oxygen knowledge log append --event decision --slug content-strategy --json` | analytics, comments, the bank, draft edits | one rule per loop | canonical voice / positioning file a proposal: `oxygen knowledge proposals approve <id> --json` | `oxygen knowledge page get content-strategy --json` | a change with no receipt behind it |\n| 9 | Hand off | Signals → inbound-led outbound | `oxygen engagement harvest --post <social_id> --source unipile --recurring --json`; `oxygen engagement status --post <social_id> --json` | the composite `social_id` from `oxygen posts get` | free; drips against the ingest budget | none — it contacts nobody | `oxygen engagement engagers --post <social_id> --json` previews the table | outreach begins here: run the inbound-led-outbound playbook |\n\n## Numeric guardrails\n\n- **Cadence and mix:** 3–5 posts/week planned; 2–3/week for four consecutive weeks is the floor before any signal is readable. At most 15% promotion posts and one lead-magnet CTA post per week. Consistency leads, reach lags.\n- **Own-feed ceiling 25 posts / 24h** per account, a platform default: own-feed publishing sits outside the outreach send quotas because it is not aimed at another member, but still refuses a sender that is not `active`. Own-post reads draw the other-API-read budget (200/day default, 1,000 max); `engagement harvest` draws the separate ingest budget (20/day, 200 max), so a big post drips over days. `oxygen engagement engagers` walks both sources up to `--max-pages` (default 5, max 20) × 100.\n- **Paid calls:** 1–5 variants per draft call (default 3), one AI call per draft and per review. Take every ceiling from the preview and `oxygen tools get <tool_id> --json`, never from prose; sweep ceilings from `oxygen blueprints preflight`.\n- **Comment SLA 48 hours**, over a rolling 30-day owned-post scope: an older thread is absent from the queue, not silent. Analytics refresh for ~30 days; reactions, comments and reshares are real; impressions come from LinkedIn's own post analytics, which it shows only to the author, so they are real for the connected member's original posts; saves and sends are not exposed.\n- **Outlier scoring:** `rank_score = 100 × √(outlier_multiple × audience_index)`, **100 is par**; a healthy sweep puts ~5–15% above 200. Defaults: `lookback_days` 7, `min_post_age_hours` 24, `min_author_followers` 1000, weights 1 / 3 / 5; 3–4 keywords (max 8), 10–25 creators (max 25).\n\n## Angle gate spec\n\nThe gate is not who gets contacted — nobody is contacted here — it is **which idea becomes a public post in the founder's name**. An item advances to draft only when all of these hold: it maps to a named pillar on `[[content-strategy]]`; it carries one concrete claim, story or artifact traceable to a wiki page, a shipped thing or a real customer outcome; it names the audience in plain words; if promotional, the week's share is still under 15%; and its voice matches the pinned voice page. The first three are enforced at drafting, because the draft call grounds on the wiki by default; voice by the free channel lint plus the paid `publishing posts review` check.\n\nNothing bypasses it, because **approval is per post and is not batchable server-side**: a draft accepted from the AI queue, a post created directly, and a row loaded by `oxygen publishing import` all land needs-approval. Borrowing from the bank has its own rule — lift the **structure**, never the text, and only from a row that cleared `rank_score` 200 with a `baseline_kind` you trust; `author_unresolved` and `maturing` rows are unscored and are not evidence.\n\n## Approval gates, mapped to who decides\n\n| Gate | What it authorises | Copilot posture |\n| --- | --- | --- |\n| kit apply (`oxygen recipes apply content-outlier-mining`) | 0-credit install of the sweep, workflow disabled | human card, once |\n| `draft-week` / `review-post` | one paid AI drafting or voice/claims call | human sets each cap from the preview |\n| `post-publish` | one public post in the founder's name, on this exact text | human, every post, never batched |\n| `comment-reply` | one public reply bound to the preview's `action_id` and `content_hash` | human approves that exact text |\n| `pilot-live` then `sweep-arm` | one live outlier cycle, then the recurring sweep | human for each; re-grant on reapply (revision-bound) |\n| amplification (`oxygen publishing amplification create`, then `enable`) | real public engagement from teammates' accounts, spending credits; LinkedIn may restrict those accounts | human twice: grant, then arm, each with `--acknowledge-risk` |\n| canonical wiki edits | voice / brand / positioning / pinned playbooks | a proposal a human approves |\n\nInstallation is never permission: the kit applies at 0 credits with its workflow disabled, and every paid run, publish, reply and armed sweep is its own gate above.\n\nReads need no approval (`oxygen publishing posts list`, `oxygen publishing analytics summary`, `oxygen senders health`, `oxygen tables query`); working wiki pages write as logged revisions.\n\n## Failure modes\n\n- **The two axes.** `--status` is pipeline position (`draft` → `scheduled` → `queued` → `published`); `--approval-status` is whether a human said yes. A queued post is both `scheduled` and `needs_approval`. Filter on the approval axis or you will report an empty queue you just filled.\n- **A bare local `--publish-at`.** With no offset it is stored as UTC; `--timezone` only changes the display. Pass an offset or `Z`.\n- **The activity URN is not the `social_id`.** `posts reactions`, `posts comments`, `engagement engagers` and `engagement harvest` all need the composite `social_id` from `oxygen posts get`. Nothing lists your own feed, so record the id when the post goes out.\n- **Mentions and content.** A verified `@<public-identifier>` goes in the post text — a structured `mentions` array is ignored and `content.mentions` rejected; approve blocks anything unresolved. `--content-json` replaces the whole content object, so a media update drops an existing `first_comment` unless you resend it.\n- **A post on a dead sender never dispatches**; fix the account instead of re-queueing. A rate limit **defers** rather than drops and names `resets_at`, so retrying in a loop only burns quota. Company-page posting is a tested-negative anti-pattern — post from the personal profile.\n- **Generic drafts are a thin wiki, not a thin prompt**; a bank where everything scores near par is a keyword naming a category. Fix the page or the phrase, never the prompt length or the weights.\n- **Outreach leaking in.** The moment the plan is to message an engager, this motion is over — hand off at stage 9. Sequences own initiation; a CRM person is created at a warm-lead gate, never because someone reacted.\n\n## Data-quality checks (after every week)\n\n1. `oxygen publishing posts list --status published --json` — every planned slot published, or one sat `needs_approval` all week? A missed slot is the failure this motion exists to prevent.\n2. `oxygen publishing comments list --view unanswered --json` — nothing older than 48h, and never `resolved` on a thread still owed an answer.\n3. `oxygen publishing analytics summary --channel linkedin --range 30d --json` — all-zero after two days means `deferred`. Then read the comments, not the counts: ICP titles, or peers and recruiters?\n4. `oxygen tables query <bank_table_id> --limit 25 --json` — mostly `author_unresolved` or `maturing` is a cold cache or short lookback; a backlog under two weeks deep means next week starts from taste.\n\n## Learning loop\n\n- **What auto-files:** a draft edit snapshots the AI's original copy, so an accepted edit records what the founder changed; `oxygen publishing drafts reject <draft_id> --reason \"...\" --json` files the reason; revisions, review findings, the metric series and the comment queue are durable.\n- **What to synthesise weekly:** which pillar produced ICP-fit comments rather than peer likes; which post *shape* travelled (a number in line one, a named enemy, a before/after); and `oxygen knowledge synthesize --kind voice --json`, a voice guide distilled from real sent copy.\n- **What to change, one rule per loop:** the pillar mix, a slot's time, a keyword on the source table (pause it, never delete it), the CTA, or the voice page. Write the change and its reason into the wiki; a voice or positioning change files a proposal.\n- **Define \"good\" first:** an inbound conversation from an ICP-fit person — a DM, a comment thread that becomes a call, a reply naming their own version of the problem. Not reach, not reactions, not a bank with more rows.\n\n## Go-live checklist\n\n1. `oxygen senders list --status active --json`, then `oxygen senders health <sender_id> --json` — one healthy account, no open checkpoint.\n2. `oxygen knowledge resolve --purpose outbound_copy --require-ready --json`, then `oxygen knowledge page upsert --slug content-strategy --type strategy --status active --body \"...\" --json`.\n3. `oxygen publishing ideas add --text \"...\" --topic <pillar> --json` until two weeks of slots exist, then draft and queue each slot (stage 3).\n4. `oxygen publishing posts list --approval-status needs_approval --json`, present the week, then `oxygen publishing posts approve <post_id> --json` one post at a time.\n5. `oxygen publishing comments list --view unanswered --json` — the 48h loop runs and a named person owns it.\n6. `oxygen recipes apply content-outlier-mining --dry-run --json`, apply, one `oxygen workflows call <workflow_id> --mode dry-run --json`, one `pilot-live` cycle, then `oxygen workflows enable <workflow_id> --approved --max-credits <cap> --json`.\n7. `oxygen budget list --json` for the org backstop, then `oxygen knowledge log append --event note --slug content-strategy --json`.\n\n## Open questions (state them, do not resolve them)\n\n- LinkedIn shows impressions only to a post's author and never exposes saves or sends. Reach is measurable for the connected member's original posts (and earned-media value once a CPM is set with `oxygen publishing analytics emv`); for anything else it stays empty. Never substitute a proxy and call it reach.\n- The cadence and mix numbers are operator estimates, not measured benchmarks. Replace them with four weeks of your own evidence.\n- The outlier kit is `beta`, and `oxygen recipes apply content-outlier-mining` installs only the keyword sweep; the creator watch applies separately against the same tables with `--table-ref`, and omitting those flags reports a table collision — the guard working.\n- Whether engagers flow automatically into outreach is not this playbook's call; stage 9 hands them over deliberately. Amplification from teammates' accounts is likewise a founder decision about the company's public name.\n\n## Resume point\n\n`oxygen recipes show founder-posting-system --json` and `oxygen knowledge page get content-strategy --json`, then `oxygen publishing posts list --json`, `oxygen publishing comments list --json` and `oxygen workflows list --json`. Start from what exists, never from scratch.\n";
|
|
17
|
+
readonly sha256: "4b182957e72e46be3ff79cf0af68ad073814c3fd88c69289c2d54aa238f1735a";
|
|
18
|
+
readonly content: "---\nname: linkedin-content-strategy\ndescription: \"Install and run the founder LinkedIn content engine: a wiki-grounded strategy page, an idea backlog, a week of drafts approved post by post, scheduled publishing, answered comments, weekly engagement read-back, outlier mining, and the hand-off of engagers to inbound-led outbound.\"\n---\n\n# Playbook: LinkedIn content strategy\n\n## Motion in one sentence\n\nA strategy page written from the wiki fixes the pillars, cadence and voice; ideas accumulate in a backlog; a week of drafts is generated from that wiki and approved one post at a time; the scheduler publishes each from the founder's own account; comments are answered through a previewed reply; engagement is read back weekly; outlier mining says which structures travelled; the lessons file back into the wiki; and the engagers become the inbound signal. Each stage reads the page the last one wrote — a posting habit without the strategy page is a treadmill, and an outlier bank without the habit is a swipe file nobody drains.\n\nRead first: `oxygen recipes show founder-posting-system --json`, `oxygen recipes show weekly-content-calendar --json`, `oxygen recipes show content-outlier-mining --json` (the stage 7 kit). Mechanics: `oxygen skills get oxygen-linkedin-marketing --json`.\n\n## Stage table\n\n| # | Stage | Owner | Install / command | Inputs | Cap | Approval | Verify | Stop |\n| --- | --- | --- | --- | --- | --- | --- | --- | --- |\n| 0 | Context + strategy | Knowledge Graph | `oxygen knowledge resolve --purpose outbound_copy --require-ready --json` → `oxygen knowledge page upsert --slug content-strategy --type strategy --title \"Content strategy\" --status active --body \"<pillars / cadence / voice>\" --json` | `positioning`, `icp`, `offers`, `voice`, all `active` | 3–5 pillars, each naming one ICP problem | none — a `strategy` page is working knowledge, logged as a revision | `oxygen knowledge page get content-strategy --json` | a stub ICP page, or a pillar with no buyer problem |\n| 1 | Sender | Publishing | `oxygen senders list --status active --json`; `oxygen senders health <sender_id> --json` (`oxygen senders limits get` for the budgets) | one connected LinkedIn account | `posts_per_day` (default 10, max 25) | none — reads | `active`, no open checkpoint | `restricted` / `credentials_required`: reconnect, never route around it |\n| 2 | Backlog | Publishing | `oxygen publishing ideas add --text \"<angle>\" --topic <pillar> --json`; `oxygen publishing ideas list --json` | angles as they occur; structures from stage 7 | two weeks of slots ahead | none (internal write) | `oxygen publishing ideas list --limit 50 --json` | an empty backlog on drafting day: mine outliers, don't draft from taste |\n| 3 | Draft + queue | Publishing | `oxygen publishing posts draft --template personal_story --topic \"<angle>\" --max-credits <cap> --json` → `oxygen publishing drafts edit <draft_id> --text-file mon.txt --json` → `oxygen publishing drafts accept <draft_id> --publish-at <iso+offset> --sender <sender_id> --json` | a pillar + a backlog angle per slot | 3–5 posts/week; 1–5 variants (default 3) | `draft-week`: paid, cap from the preview; queueing needs none — it never sends | `oxygen publishing posts list --approval-status needs_approval --json` | a bare `--publish-at`: with no offset it is stored as UTC |\n| 4 | Review + publish | Publishing | `oxygen publishing posts review <post_id> --max-credits <cap> --json`; `oxygen publishing mentions resolve --text-file mon.txt --json` → `oxygen publishing posts approve <post_id> --json` | the exact final text, read by the founder | ≤15% promotion posts; ≤1 lead-magnet CTA/week | `review-post`: paid, advisory. `post-publish`: human, every post, never batched | `oxygen publishing posts get <post_id> --json` — attempts, provider id, deep-link | an unresolved `@<public-identifier>`; `linkedin_rate_limited` |\n| 5 | Comments | Publishing | `oxygen publishing comments list --view unanswered --json` → `oxygen publishing comments reply <comment_id> --text-file reply.txt --json` → `oxygen publishing comments approve <action_id> --content-hash <sha256> --approved --json` → `oxygen publishing comments resolve <comment_id> --json` | the unanswered queue (30-day scope) | one reply per comment, inside 48h | `comment-reply`: human approves that exact previewed text and its hash | the queue drains; handled items read `resolved` | a comment older than 48h with no reply |\n| 6 | Read back | Publishing + Posts | `oxygen publishing analytics summary --channel linkedin --range 30d --json` (winners, `leader` lead, `null_reasons`); `oxygen publishing analytics timeseries --channel linkedin --range 30d --json` (earned per day, growing or shrinking vs the previous period, follower change); `oxygen publishing analytics post <post_id> --json`; `oxygen posts get --post <post_url> --json` | published posts | metrics refresh ~30 days after publish | none — reads | reactions, comments, reshares and — on your own original posts — impressions present; reposts counted in `reposts_excluded`, not in totals | every metric zero after two days: confirm `published`, not `deferred` |\n| 7 | Mine outliers | Workflows — kit stage `linkedin-keyword-outliers` | `oxygen recipes apply content-outlier-mining --dry-run --json` → without `--dry-run`; then `oxygen blueprints apply linkedin-creator-outliers --table-ref content_outliers=<bank_table_id> --json` | 3–4 buyer-language keywords; ≤25 public `/in/` URLs | 7-day lookback; 1 page/keyword on the pilot; ceiling from preflight | `pilot-live` then `sweep-arm`: human, re-granted on reapply | `oxygen workflows tail <run_id> --json`; `oxygen tables query <bank_table_id> --json` | everything near par, or top rows are hiring posts |\n| 8 | Learn | Knowledge Graph | `oxygen knowledge synthesize --kind voice --json` → `--approved`; `oxygen knowledge log append --event decision --slug content-strategy --json` | analytics, comments, the bank, draft edits | one rule per loop | canonical voice / positioning file a proposal: `oxygen knowledge proposals approve <id> --json` | `oxygen knowledge page get content-strategy --json` | a change with no receipt behind it |\n| 9 | Hand off | Signals → inbound-led outbound | `oxygen engagement harvest --post <social_id> --source unipile --recurring --json`; `oxygen engagement status --post <social_id> --json` | the composite `social_id` from `oxygen posts get` | free; drips against the ingest budget | none — it contacts nobody | `oxygen engagement engagers --post <social_id> --json` previews the table | outreach begins here: run the inbound-led-outbound playbook |\n\n## Numeric guardrails\n\n- **Cadence and mix:** 3–5 posts/week planned; 2–3/week for four consecutive weeks is the floor before any signal is readable. At most 15% promotion posts and one lead-magnet CTA post per week. Consistency leads, reach lags.\n- **Own-feed cap `posts_per_day`** per account (default 10, max 25): own-feed publishing sits outside the outreach send quotas because it is not aimed at another member, but still refuses a sender that is not `active`. Own-post reads draw the other-API-read budget (200/day default, 1,000 max); `engagement harvest` draws the separate ingest budget (20/day, 200 max), so a big post drips over days. `oxygen engagement engagers` walks both sources up to `--max-pages` (default 5, max 20) × 100.\n- **Paid calls:** 1–5 variants per draft call (default 3), one AI call per draft and per review. Take every ceiling from the preview and `oxygen tools get <tool_id> --json`, never from prose; sweep ceilings from `oxygen blueprints preflight`.\n- **Comment SLA 48 hours**, over a rolling 30-day owned-post scope: an older thread is absent from the queue, not silent. Analytics refresh for ~30 days; reactions, comments and reshares are real; impressions come from LinkedIn's own post analytics, which it shows only to the author, so they are real for the connected member's original posts; saves and sends are not exposed.\n- **Outlier scoring:** `rank_score = 100 × √(outlier_multiple × audience_index)`, **100 is par**; a healthy sweep puts ~5–15% above 200. Defaults: `lookback_days` 7, `min_post_age_hours` 24, `min_author_followers` 1000, weights 1 / 3 / 5; 3–4 keywords (max 8), 10–25 creators (max 25).\n\n## Angle gate spec\n\nThe gate is not who gets contacted — nobody is contacted here — it is **which idea becomes a public post in the founder's name**. An item advances to draft only when all of these hold: it maps to a named pillar on `[[content-strategy]]`; it carries one concrete claim, story or artifact traceable to a wiki page, a shipped thing or a real customer outcome; it names the audience in plain words; if promotional, the week's share is still under 15%; and its voice matches the pinned voice page. The first three are enforced at drafting, because the draft call grounds on the wiki by default; voice by the free channel lint plus the paid `publishing posts review` check.\n\nNothing bypasses it, because **approval is per post and is not batchable server-side**: a draft accepted from the AI queue, a post created directly, and a row loaded by `oxygen publishing import` all land needs-approval. Borrowing from the bank has its own rule — lift the **structure**, never the text, and only from a row that cleared `rank_score` 200 with a `baseline_kind` you trust; `author_unresolved` and `maturing` rows are unscored and are not evidence.\n\n## Approval gates, mapped to who decides\n\n| Gate | What it authorises | Copilot posture |\n| --- | --- | --- |\n| kit apply (`oxygen recipes apply content-outlier-mining`) | 0-credit install of the sweep, workflow disabled | human card, once |\n| `draft-week` / `review-post` | one paid AI drafting or voice/claims call | human sets each cap from the preview |\n| `post-publish` | one public post in the founder's name, on this exact text | human, every post, never batched |\n| `comment-reply` | one public reply bound to the preview's `action_id` and `content_hash` | human approves that exact text |\n| `pilot-live` then `sweep-arm` | one live outlier cycle, then the recurring sweep | human for each; re-grant on reapply (revision-bound) |\n| amplification (`oxygen publishing amplification create`, then `enable`) | real public engagement from teammates' accounts, spending credits; LinkedIn may restrict those accounts | human twice: grant, then arm, each with `--acknowledge-risk` |\n| canonical wiki edits | voice / brand / positioning / pinned playbooks | a proposal a human approves |\n\nInstallation is never permission: the kit applies at 0 credits with its workflow disabled, and every paid run, publish, reply and armed sweep is its own gate above.\n\nReads need no approval (`oxygen publishing posts list`, `oxygen publishing analytics summary`, `oxygen senders health`, `oxygen tables query`); working wiki pages write as logged revisions.\n\n## Failure modes\n\n- **The two axes.** `--status` is pipeline position (`draft` → `scheduled` → `queued` → `published`); `--approval-status` is whether a human said yes. A queued post is both `scheduled` and `needs_approval`. Filter on the approval axis or you will report an empty queue you just filled.\n- **A bare local `--publish-at`.** With no offset it is stored as UTC; `--timezone` only changes the display. Pass an offset or `Z`.\n- **The activity URN is not the `social_id`.** `posts reactions`, `posts comments`, `engagement engagers` and `engagement harvest` all need the composite `social_id` from `oxygen posts get`. Nothing lists your own feed, so record the id when the post goes out.\n- **Mentions and content.** A verified `@<public-identifier>` goes in the post text — a structured `mentions` array is ignored and `content.mentions` rejected; approve blocks anything unresolved. `--content-json` replaces the whole content object, so a media update drops an existing `first_comment` unless you resend it.\n- **A post on a dead sender never dispatches**; fix the account instead of re-queueing. A rate limit **defers** rather than drops and names `resets_at`, so retrying in a loop only burns quota. Company-page posting is a tested-negative anti-pattern — post from the personal profile.\n- **Generic drafts are a thin wiki, not a thin prompt**; a bank where everything scores near par is a keyword naming a category. Fix the page or the phrase, never the prompt length or the weights.\n- **Outreach leaking in.** The moment the plan is to message an engager, this motion is over — hand off at stage 9. Sequences own initiation; a CRM person is created at a warm-lead gate, never because someone reacted.\n\n## Data-quality checks (after every week)\n\n1. `oxygen publishing posts list --status published --json` — every planned slot published, or one sat `needs_approval` all week? A missed slot is the failure this motion exists to prevent.\n2. `oxygen publishing comments list --view unanswered --json` — nothing older than 48h, and never `resolved` on a thread still owed an answer.\n3. `oxygen publishing analytics summary --channel linkedin --range 30d --json` — all-zero after two days means `deferred`. Then read the comments, not the counts: ICP titles, or peers and recruiters?\n4. `oxygen tables query <bank_table_id> --limit 25 --json` — mostly `author_unresolved` or `maturing` is a cold cache or short lookback; a backlog under two weeks deep means next week starts from taste.\n\n## Learning loop\n\n- **What auto-files:** a draft edit snapshots the AI's original copy, so an accepted edit records what the founder changed; `oxygen publishing drafts reject <draft_id> --reason \"...\" --json` files the reason; revisions, review findings, the metric series and the comment queue are durable.\n- **What to synthesise weekly:** which pillar produced ICP-fit comments rather than peer likes; which post *shape* travelled (a number in line one, a named enemy, a before/after); and `oxygen knowledge synthesize --kind voice --json`, a voice guide distilled from real sent copy.\n- **What to change, one rule per loop:** the pillar mix, a slot's time, a keyword on the source table (pause it, never delete it), the CTA, or the voice page. Write the change and its reason into the wiki; a voice or positioning change files a proposal.\n- **Define \"good\" first:** an inbound conversation from an ICP-fit person — a DM, a comment thread that becomes a call, a reply naming their own version of the problem. Not reach, not reactions, not a bank with more rows.\n\n## Go-live checklist\n\n1. `oxygen senders list --status active --json`, then `oxygen senders health <sender_id> --json` — one healthy account, no open checkpoint.\n2. `oxygen knowledge resolve --purpose outbound_copy --require-ready --json`, then `oxygen knowledge page upsert --slug content-strategy --type strategy --status active --body \"...\" --json`.\n3. `oxygen publishing ideas add --text \"...\" --topic <pillar> --json` until two weeks of slots exist, then draft and queue each slot (stage 3).\n4. `oxygen publishing posts list --approval-status needs_approval --json`, present the week, then `oxygen publishing posts approve <post_id> --json` one post at a time.\n5. `oxygen publishing comments list --view unanswered --json` — the 48h loop runs and a named person owns it.\n6. `oxygen recipes apply content-outlier-mining --dry-run --json`, apply, one `oxygen workflows call <workflow_id> --mode dry-run --json`, one `pilot-live` cycle, then `oxygen workflows enable <workflow_id> --approved --max-credits <cap> --json`.\n7. `oxygen budget list --json` for the org backstop, then `oxygen knowledge log append --event note --slug content-strategy --json`.\n\n## Open questions (state them, do not resolve them)\n\n- LinkedIn shows impressions only to a post's author and never exposes saves or sends. Reach is measurable for the connected member's original posts (and earned-media value once a CPM is set with `oxygen publishing analytics emv`); for anything else it stays empty. Never substitute a proxy and call it reach.\n- The cadence and mix numbers are operator estimates, not measured benchmarks. Replace them with four weeks of your own evidence.\n- The outlier kit is `beta`, and `oxygen recipes apply content-outlier-mining` installs only the keyword sweep; the creator watch applies separately against the same tables with `--table-ref`, and omitting those flags reports a table collision — the guard working.\n- Whether engagers flow automatically into outreach is not this playbook's call; stage 9 hands them over deliberately. Amplification from teammates' accounts is likewise a founder decision about the company's public name.\n\n## Resume point\n\n`oxygen recipes show founder-posting-system --json` and `oxygen knowledge page get content-strategy --json`, then `oxygen publishing posts list --json`, `oxygen publishing comments list --json` and `oxygen workflows list --json`. Start from what exists, never from scratch.\n";
|
|
19
19
|
}, {
|
|
20
20
|
readonly slug: "inbound-led-outbound";
|
|
21
21
|
readonly title: "Playbook: Inbound-led outbound";
|
|
@@ -14,15 +14,15 @@ export const COPILOT_SKILL_SNAPSHOTS_GENERATED = [
|
|
|
14
14
|
slug: "tam-sourcing",
|
|
15
15
|
title: "Playbook: TAM sourcing",
|
|
16
16
|
sources: ["oxygen-playbooks/playbooks/tam-sourcing.md"],
|
|
17
|
-
sha256: "
|
|
18
|
-
content: "---\nname: tam-sourcing\ndescription: \"Turn the workspace ICP into a sourced, scored, deduplicated account map and the buyer personas at those accounts — a bounded sample first, coverage widened only on segments the sample proved — then hand it to outbound. Load it when the whole market map is the ask.\"\n---\n\n# Playbook: TAM sourcing\n\n## Motion in one sentence\n\nThe ICP compiles into a provider-grounded sourcing plan, one bounded sample of accounts is sourced live, deduplicated and filled, every account is scored against that same ICP with its evidence on the row, the sample's band distribution decides which segments deserve coverage, coverage widens one segment at a time through the same gate, buyer personas are sourced only at accounts that passed it, and the accepted map is handed to an outbound motion. The sample before the coverage is the play; the order is companies → fit → people, never people first.\n\nRecipe and kit: `oxygen recipes show icp-to-account-map --json` (its `journey.slug` is `tam-sourcing`); the companion it hands to is `oxygen recipes show outbound-pilot-50 --json`. Read both first — `recipes show` reports which stages this workspace already has.\n\n## Stage table\n\n| # | Stage | Owner | Install / command | Inputs | Cap | Approval | Verify | Stop |\n| --- | --- | --- | --- | --- | --- | --- | --- | --- |\n| 0 | Context | Knowledge Graph | `oxygen knowledge page get icp --json`; `oxygen knowledge page get offers --json` (`oxygen knowledge status` reports the local CLI mirror, not the workspace) | `company`, `offering`, `icp` pages | — | none | pages `active`, not seed stubs; the ICP names segment, geography, size **and who is out** | a stub ICP, or no exclusion clause |\n| 1 | Plan | Tables — company-search planner | `oxygen tools enums get blitzapi industry --query <segment> --json` → `oxygen companies search plan --prompt \"<ICP sentence with EXCLUDE clause>\" --filters-json '<typed filters>' --estimate --json` | one ICP sentence; exact enum strings | `--target-count` ceiling 50,000 per plan | none — no provider call, no spend | `filter_application[].dropped`, `provider_availability`, `estimated_match_count.basis`, `recommended_live_route_id` | a filter you need lands in `dropped_constraints`; a managed primary reads `degraded: true` |\n| 2 | Scaffold | Tables — kit stage `account-sourcing` | `oxygen recipes apply icp-to-account-map --dry-run --json` → the same without `--dry-run` | — | 0 credits, no provider call, no external write | `kit-apply`: human, once for the kit | `oxygen tables describe <accounts> --json` shows `company_name`, `domain`, `company_linkedin_url`, `source`, `fit_notes` | a stage already installed — never re-install it |\n| 3 | Source the sample | Tables — company search | `oxygen companies search run --plan-json <plan> --route-id <route_id> --table <accounts> --upsert-key domain --mode dry_run --json` → `--mode live --max-pages 1 --max-credits <cap> --approved` | the plan you actually read | one page, the recipe's 25-account pilot; `<cap>` from the route estimate | `source-sample`: paid, human, server-enforced | `oxygen table-ingestions wait <ingestion_run_id> --json`; `oxygen tables query <accounts> --limit 25 --json` | `spend_cap_too_low`; near-zero rows; `data_status` still `pending` — the verdict is on the ingestion run, not the queue receipt |\n| 4 | Clean | Tables | `oxygen tables dedupe <accounts> --on domain --normalize domain --json` → `--apply --approved --merge-values fill-empty`; `oxygen tables auto-dedupe set <accounts> --on domain --normalize domain --json`; `oxygen companies enrich preview <accounts> --missing-fields domain,linkedin_url,headcount,industry --json` → `oxygen companies enrich run <accounts> --mode live --max-credits <cap> --approved --json` | `domain`; rows with firmographic gaps | `--scan-limit` 50,000 / 200,000 cap; enrichment scoped by its preview | dedupe apply: free internal delete, losers kept in row history. `fill-firmographics`: paid, human | dedupe preview reruns to 0 groups; `oxygen cells inspect <accounts> <row_id> domain --json` carries provider and cost | groups that are not one company; a provider in the preview reading blocked or benched |\n| 5 | Score | Tables — kit stage `icp-fit-scoring` | `oxygen columns run <accounts> icp_fit_score --limit 10 --dry-run --json` → `--limit 25 --background --approved --max-credits <cap> --json` | the `icp` page (auto-prepended), `company_name`, `domain` | the sample only; `--limit` defaults to 10 | `score-accounts`: paid, human for the cap | `oxygen cells inspect <accounts> <row_id> icp_fit_score --json` — one high, one medium, one low, each `evidence` citing a checkable fact | bands spread evenly, or evidence reads generic: fix the ICP page, not the prompt |\n| 6 | Widen | Tables — company search, again | re-plan the surviving segment, then the stage-3 live command with the new plan and `--max-pages <n>` | segments whose sample cleared the widen bar | one segment per widen; auto-pagination stops at the cap | `widen-coverage`: paid, human, **per segment** | the `auto_paged` block; `oxygen lead-sourcing audit <accounts> --spec ./icp-spec.json --json` | a segment whose sample landed mostly `low` — more pages of a bad filter is more junk |\n| 7 | Personas | Tables — Blueprint `contact-finding` | `oxygen blueprints apply contact-finding --json` → `oxygen people search plan --company-domains <accepted-domains> --titles \"<persona>\" --title-match exact --max-per-company <n> --json` → `oxygen people search run --plan-json <plan> --route-id <route_id> --table <contacts> --upsert-key linkedin_url --mode live --max-credits <cap> --approved --json` | the accepted-domain list from the gate below; one persona title set | `--max-per-company <n>`; upsert dedupes on `linkedin_url` | `source-personas`: paid, human | `oxygen tables query <contacts> --limit 25 --json`; `oxygen tables link <contacts> --to <accounts> --on company_domain --json` → `--approved` | a route whose `tool_access` is not `runnable`; any route that drops `company.domains` |\n| 8 | Hand off and learn | Sequences / Records / Knowledge Graph | `oxygen recipes show outbound-pilot-50 --json`; `oxygen tables promote <accounts> --object companies --dry-run --json` → `--approved`; `oxygen knowledge log append --event decision --slug account-map --summary \"...\" --json` | accepted accounts, their contacts, the run receipts | the receiving motion's caps; one rule changed per loop | `outbound-enrollment` belongs to that motion, never this one; canonical pages route through the proposal queue | `oxygen crm pipeline --json` after the first warm lead; `oxygen knowledge lint --json` | enrolling the contacts table wholesale — sourcing never initiates outreach |\n\n## Numeric guardrails\n\n- **Sample: 25 accounts, one page** — the recipe's `pilot.size`; `--max-pages 1` stops auto-pagination widening a filter nobody has read.\n- **Plan ceiling: 50,000** per company or people plan (`--target-count`); above it the plan returns a clamp warning and segmentation guidance. Segment instead of raising it.\n- **Bands.** `icp_fit_score` returns `band` (high / medium / low), a 0-100 `score` and one `evidence` sentence; borderline accounts land `medium`, never a generous `high`.\n- **Widen bar.** Widen only when a segment's sample clears the bar you wrote down; the recipe's default is high + medium above 50%. Plausible-fit on page 1 runs 60-80% for a sharp ICP, under ~40% means loose filters — both carry `benchmark_basis: estimate`, so they calibrate, they do not measure.\n- **Scope defaults.** Dedupe `--scan-limit` is 50,000 rows (cap 200,000), and standing auto-dedupe runs **before** auto-run so enrichment is never queued for rows about to be deleted. `columns run --limit` defaults to 10, inline deterministic runs cap at 25, `--all` requires `--background`. Personas per account have no default — set `--max-per-company` deliberately. Capacity: 3M rows per Table, 25M per workspace (`oxygen limits show --json`).\n- **Every `--max-credits <cap>` comes from the immediately preceding preview**, route estimate or `recommended_max_credits`. A too-low cap on a waterfall does not stop the run: the expensive lane is refused on its own and the cascade advances, so a cheaper lane still bills while the good one is skipped. Premium managed lanes stay off unless `--allow-premium-lanes` is passed; read a lane's price from `oxygen tools get <tool_id> --json`, never from prose.\n- **`domain` is the account identity, `linkedin_url` the person identity** — both are the upsert keys, neither is overwritten, and provenance accumulates in `source`.\n\n## Accepted-account gate spec\n\nAn account reaches persona sourcing only when `icp_fit_score.band` is `high`, or `medium` **with** an evidence string naming a verifiable fact; `score` is at or above the band floor written into the account-map page; `domain` is present, canonical, and not a directory, aggregator or search URL; the row is not excluded by the ICP spec (competitor, current customer, wrong geography, wrong size); and the row survived dedupe.\n\nEnforcement is a formula column, not prose. Validate it free, attach it, then materialise it — formula values compute when read, so a filter on one is refused by default:\n\n```bash\noxygen formulas validate <accounts> --expression 'if(and(or(path(icp_fit_score, \"band\") == \"high\", path(icp_fit_score, \"band\") == \"medium\"), is_blank(excluded_reason)), \"accepted\", \"held\")' --rows 5 --json\noxygen columns add <accounts> --kind formula --key account_accepted --label \"Accepted\" --definition-json '<validated expression>' --json\noxygen columns run <accounts> account_accepted --force --json\noxygen tables query <accounts> --filter-json '{\"column\":\"account_accepted\",\"op\":\"eq\",\"value\":\"accepted\"}' --formula-values materialized --fields domain --limit 1000 --json\noxygen tables views create <accounts> --name \"Accepted accounts\" --json\n```\n\nNothing bypasses this: never hand the whole accounts table to `oxygen people search run`, never source people at an account whose `icp_fit_score` cell is empty or errored, and never promote a `held` row because the market looks thin. A thin accepted set is a filter or ICP problem, fixed upstream.\n\n## Approval gates, mapped to who decides\n\n| Gate | What it authorises | Attended Copilot | Unattended run |\n| --- | --- | --- | --- |\n| `kit-apply` | 0-credit install of the accounts table and scoring column | auto-approvable workspace write, one card | installs only; arms nothing |\n| `source-sample` | one live company-search page under a cap | human card — provider spend | its own approved per-delivery ceiling |\n| dedupe `--apply --approved` | deleting duplicates (recoverable from row history) | auto-approvable workspace write | standing auto-dedupe, no per-write approval |\n| `fill-firmographics` | one capped company-enrichment run | human card | its own ceiling |\n| `score-accounts` | one paid AI column run under a cap | human card for the cap | its own ceiling; `tables auto-run set` is scoped standing permission for the listed columns |\n| `widen-coverage` | one more segment, re-granted every time | human card, per segment | never standing — a widen is a new purchase |\n| `source-personas` | one live people-search run scoped to accepted domains | human card | its own ceiling |\n| `tables promote --approved` | writing table columns onto matched CRM records | auto-approvable internal truth write | free, still explicit |\n| `outbound-enrollment` | contacting anyone at all | owned by the outbound motion | never granted here |\n\nReads — `recipes show`, both `search plan` commands, `tables query`, `cells inspect`, `lead-sourcing audit`, `budget list`, `limits show` — need no approval and spend nothing. Canonical wiki edits file a proposal a human approves.\n\n## Failure modes\n\n- **Free-text keyword where the provider wants an enum** → `invalid_provider_enum_value`, or worse a noisy page. Fetch the catalog and pass the exact string; free text only tightens an already enum-grounded segment.\n- **A dropped filter read as applied, or an invented market size.** `filter_application[]` reports applied vs dropped per route with the reason; quote `estimated_match_count` only when its `basis` is `provider_count` — any other basis is derived from your requested target, not from the market.\n- **Scoring the whole table before reading page 1** — the failure this motion exists to prevent: it spends several times over before anyone knows the filters work.\n- **Filtering a formula column.** `tables query` refuses formula filters by default because displayed formulas evaluate live. Run with `--force` (free), then `--formula-values materialized`.\n- **Appending instead of merging.** A source run without `--upsert-key domain` re-adds the same companies every page; collapse them, then arm the standing auto-dedupe. Enriching before deduping pays twice for one company.\n- **A benched or unknown managed provider.** `provider_availability` carries `degraded: true` or `availability: \"unknown\"`, and a live run refuses a benched managed primary before table creation. Read each entry's `next_action` — funding clears neither a staff hold nor a rejected key, and an unknown snapshot is re-previewed, not assumed.\n- **A preview-only people route treated as runnable.** Apollo and ContactOut people search return masked records without the stable `linkedin_url` the Contacts upsert contract needs, so `recommended_live_route_id` can name the right contract without naming a runnable route. Confirm `tool_access` is `runnable`, and never accept a route that drops `company.domains` — that is the gate leaking.\n- **An AI column asked a web question.** AI columns have no web access and answer from the row; web answers belong in a `--kind research` column with a `--research-query`. And a `queued` / `not_started` run has captured nothing yet — `oxygen table-runs cancel <run_id> --json` before pickup leaves spend at 0.\n\n## Data-quality checks (after every sourcing cycle)\n\n1. `oxygen tables query <accounts> --limit 100 --json` — rows with a blank `domain`, or one that is a directory, aggregator or search URL. Clear those cells **and** the row's `icp_fit_score` before re-scoring; poisoned cells are not re-scored on their own.\n2. `oxygen tables dedupe <accounts> --on domain --normalize domain --json` — zero groups once auto-dedupe is armed; anything else means a write path bypassed the key.\n3. `oxygen lead-sourcing audit <accounts> --spec ./icp-spec.json --json` — a reason on every exclusion; an exclusion with no reason is a filter you cannot defend.\n4. `oxygen cells inspect <accounts> <row_id> icp_fit_score --json` on one high, one medium and one low row; evidence citing nothing checkable means a thin ICP page.\n5. `oxygen tables query <contacts> --limit 100 --json` — contacts per accepted account; zero is a persona-filter problem, not a market problem. `oxygen table-runs provider-summary <run_id> --json` says which provider answered and at what cost per row.\n\n## Learning loop\n\n- **What auto-files:** the ingestion run and its item outputs, per-cell provider and cost provenance, the knowledge log line each kit apply writes, `table-runs provider-summary`, and the audit output.\n- **What to synthesise monthly:** high-fit share by segment; contacts per accepted account; credits per *accepted account* (not per sourced row — the denominator that matters is the account someone would work); and, once the outbound companion has run, reply rate by segment joined back to the map.\n- **What to change, one rule per loop:** one filter, one band floor, one excluded segment, or one persona title set. Write the change and its reason into the account-map page; canonical positioning files a proposal instead.\n- **\"Good\", written before the first live run:** an accepted account is one with a reachable buyer persona in a segment where you can name a reason you win. Coverage without that definition is a row count, not a market map.\n\n## Go-live checklist\n\n1. `oxygen knowledge page get icp --json` and `oxygen knowledge page get offers --json` — both `active`, the ICP sentence carrying an explicit EXCLUDE clause. (`oxygen knowledge status --json` describes the local CLI mirror and can report zero pages while the workspace is filled; it is not the readiness check.)\n2. `oxygen integrations list --json` and `oxygen billing balance --json` — a company-search provider reachable, headroom for the sample.\n3. `oxygen tools enums get blitzapi industry --query <segment> --json`, then `oxygen companies search plan --prompt \"<ICP sentence>\" --filters-json '<typed filters>' --estimate --json`; read `filter_application` and `provider_availability`, save the plan to a file.\n4. `oxygen recipes apply icp-to-account-map --dry-run --json`, then apply — table and `icp_fit_score` installed, nothing armed.\n5. `source-sample` granted; `oxygen table-ingestions wait <ingestion_run_id> --json` green; 25 rows readable.\n6. Dedupe applied and `oxygen tables auto-dedupe set <accounts> --on domain --normalize domain --json` armed; `fill-firmographics` and `score-accounts` granted; one high, one medium and one low read by hand.\n7. `account_accepted` validated, attached, materialised; `oxygen tables views create <accounts> --name \"Accepted accounts\" --json`.\n8. Widen bar written down; `widen-coverage` granted for the first segment only.\n9. `oxygen blueprints apply contact-finding --json`; `source-personas` for one persona set at accepted domains; `oxygen tables link <contacts> --to <accounts> --on company_domain --json` previewed, then `--approved`.\n10. `oxygen budget list --json` and `oxygen limits show --json` — org backstop and capacity headroom in place; `oxygen knowledge page upsert --slug account-map --type research_note --json` records segments, filters, band floor and widen decisions.\n\n## Open questions (state them, do not resolve them)\n\n- Company search publishes no `schedulable` field the way signal search does, so a recurring refresh is discovered by attempting `oxygen feeds bind <accounts> --kind company_search --upsert-key domain --every daily@9 --max-credits <cap> --approved --json` and treating a `feed_not_incremental` refusal as the answer. Until then the supported cadence is a monthly re-plan through the same gates.\n- The recipe's fit bands are `benchmark_basis: estimate` with no workspace-measured baseline behind them, and the canonical ICP scorer function is not yet ratifiable — this playbook uses the kit's table AI column, never a callable.\n- Whether accepted accounts should become CRM company records at map time is unratified. This playbook keeps the map in Tables and promotes only accounts a human is working; a CRM *person* is created at the warm-lead gate downstream, never at sourcing time.\n- Cross-table suppression is read-only through `oxygen tables dedupe <accounts> --on domain --against <suppression-table> --against-column domain --json`; no standing suppression policy exists on a source run, so exclusions live in the plan's EXCLUDE clause and the audit spec.\n\n## Resume point\n\n`oxygen recipes show icp-to-account-map --json`. Its `kit` block names each stage's install status; `oxygen tables list --json` says whether an accounts or contacts table already exists, and `oxygen tables auto-dedupe get <accounts> --json` plus `oxygen tables auto-run get <accounts> --json` say what already runs on writes. Start there, never from scratch.\n\n## What the human still has to do\n\nNothing above contacts anyone, and nothing widens on its own. The human writes the ICP sentence and band floor into `oxygen knowledge page upsert --slug account-map --json`; grants `source-sample`, `fill-firmographics` and `score-accounts`, each with a cap read from its own preview; reads one high, one medium and one low row with `oxygen cells inspect <accounts> <row_id> icp_fit_score --json` and decides whether the filters or the ICP page need fixing; grants `widen-coverage` once per segment; grants `source-personas` after choosing the persona titles and `--max-per-company`; and decides whether the accepted map goes to `outbound-pilot-50` now or after another coverage pass. That read is the step routinely skipped, and the one the whole motion is built around.\n",
|
|
17
|
+
sha256: "f3743642d6b0a1f91d66ea674ea56c3479e34ee840c89f043a2bbfc94dd6efb6",
|
|
18
|
+
content: "---\nname: tam-sourcing\ndescription: \"Turn the workspace ICP into a sourced, scored, deduplicated account map and the buyer personas at those accounts — a bounded sample first, coverage widened only on segments the sample proved — then hand it to outbound. Load it when the whole market map is the ask.\"\n---\n\n# Playbook: TAM sourcing\n\n## Motion in one sentence\n\nThe ICP compiles into a provider-grounded sourcing plan, one bounded sample of accounts is sourced live, deduplicated and filled, every account is scored against that same ICP with its evidence on the row, the sample's band distribution decides which segments deserve coverage, coverage widens one segment at a time through the same gate, buyer personas are sourced only at accounts that passed it, and the accepted map is handed to an outbound motion. The sample before the coverage is the play; the order is companies → fit → people, never people first.\n\nRecipe and kit: `oxygen recipes show icp-to-account-map --json` (its `journey.slug` is `tam-sourcing`); the companion it hands to is `oxygen recipes show outbound-pilot-50 --json`. Read both first — `recipes show` reports which stages this workspace already has.\n\n## Stage table\n\n| # | Stage | Owner | Install / command | Inputs | Cap | Approval | Verify | Stop |\n| --- | --- | --- | --- | --- | --- | --- | --- | --- |\n| 0 | Context | Knowledge Graph | `oxygen knowledge page get icp --json`; `oxygen knowledge page get offers --json` (`oxygen knowledge status` reports the local CLI mirror, not the workspace) | `company`, `offering`, `icp` pages | — | none | pages `active`, not seed stubs; the ICP names segment, geography, size **and who is out** | a stub ICP, or no exclusion clause |\n| 1 | Plan | Tables — company-search planner | `oxygen tools enums get blitzapi industry --query <segment> --json` → `oxygen companies search plan --prompt \"<ICP sentence with EXCLUDE clause>\" --filters-json '<typed filters>' --estimate --json` | one ICP sentence; exact enum strings | `--target-count` ceiling 50,000 per plan | none — no provider call, no spend | `filter_application[].dropped`, `provider_availability`, `estimated_match_count.basis`, `recommended_live_route_id` | a filter you need lands in `dropped_constraints`; a managed primary reads `degraded: true` |\n| 2 | Scaffold | Tables — kit stage `account-sourcing` | `oxygen recipes apply icp-to-account-map --dry-run --json` → the same without `--dry-run` | — | 0 credits, no provider call, no external write | `kit-apply`: human, once for the kit | `oxygen tables describe <accounts> --json` shows `company_name`, `domain`, `company_linkedin_url`, `source`, `fit_notes` | a stage already installed — never re-install it |\n| 3 | Source the sample | Tables — company search | `oxygen companies search run --plan-json <plan> --route-id <route_id> --table <accounts> --upsert-key domain --mode dry_run --json` → `--mode live --max-pages 1 --max-credits <cap> --approved` | the plan you actually read | one page, the recipe's 25-account pilot; `<cap>` from the route estimate | `source-sample`: paid, human, server-enforced | `oxygen table-ingestions wait <ingestion_run_id> --json`; `oxygen tables query <accounts> --limit 25 --json` | `spend_cap_too_low`; near-zero rows; `data_status` still `pending` — the verdict is on the ingestion run, not the queue receipt |\n| 4 | Clean | Tables | `oxygen tables dedupe <accounts> --on domain --normalize domain --json` → `--apply --approved --merge-values fill-empty`; `oxygen tables auto-dedupe set <accounts> --on domain --normalize domain --json`; `oxygen companies enrich preview <accounts> --missing-fields domain,linkedin_url,headcount,industry --json` → `oxygen companies enrich run <accounts> --mode live --max-credits <cap> --approved --json` | `domain`; rows with firmographic gaps | `--scan-limit` 50,000 / 200,000 cap; enrichment scoped by its preview | dedupe apply: free internal delete, losers kept in row history. `fill-firmographics`: paid, human | dedupe preview reruns to 0 groups; `oxygen cells inspect <accounts> <row_id> domain --json` carries provider and cost | groups that are not one company; a provider in the preview reading blocked or benched |\n| 5 | Score | Tables — kit stage `icp-fit-scoring` | `oxygen columns run <accounts> icp_fit_score --limit 10 --dry-run --json` → `--limit 25 --background --approved --max-credits <cap> --json` | the `icp` page (auto-prepended), `company_name`, `domain` | the sample only; `--limit` defaults to 10 | `score-accounts`: paid, human for the cap | `oxygen cells inspect <accounts> <row_id> icp_fit_score --json` — one high, one medium, one low, each `evidence` citing a checkable fact | bands spread evenly, or evidence reads generic: fix the ICP page, not the prompt |\n| 6 | Widen | Tables — company search, again | re-plan the surviving segment, then the stage-3 live command with the new plan and `--max-pages <n>` | segments whose sample cleared the widen bar | one segment per widen; auto-pagination stops at the cap | `widen-coverage`: paid, human, **per segment** | the `auto_paged` block; `oxygen lead-sourcing audit <accounts> --spec ./icp-spec.json --json` | a segment whose sample landed mostly `low` — more pages of a bad filter is more junk |\n| 7 | Personas | Tables — Blueprint `contact-finding` | `oxygen blueprints apply contact-finding --json` → `oxygen people search plan --company-domains <accepted-domains> --titles \"<persona>\" --title-match exact --max-per-company <n> --json` → `oxygen people search run --plan-json <plan> --route-id <route_id> --table <contacts> --upsert-key linkedin_url --mode live --max-credits <cap> --approved --json` | the accepted-domain list from the gate below; one persona title set | `--max-per-company <n>`; upsert dedupes on `linkedin_url` | `source-personas`: paid, human | `oxygen tables query <contacts> --limit 25 --json`; `oxygen tables link <contacts> --to <accounts> --on company_domain --json` → `--approved` | a route whose `tool_access` is not `runnable`; any route that drops `company.domains` |\n| 8 | Hand off and learn | Sequences / Records / Knowledge Graph | `oxygen recipes show outbound-pilot-50 --json`; `oxygen tables promote <accounts> --object companies --dry-run --json` → `--approved`; `oxygen knowledge log append --event decision --slug account-map --summary \"...\" --json` | accepted accounts, their contacts, the run receipts | the receiving motion's caps; one rule changed per loop | `outbound-enrollment` belongs to that motion, never this one; canonical pages route through the proposal queue | `oxygen crm pipeline --json` after the first warm lead; `oxygen knowledge lint --json` | enrolling the contacts table wholesale — sourcing never initiates outreach |\n\n## Numeric guardrails\n\n- **Sample: 25 accounts, one page** — the recipe's `pilot.size`; `--max-pages 1` stops auto-pagination widening a filter nobody has read.\n- **Plan ceiling: 50,000** per company or people plan (`--target-count`); above it the plan returns a clamp warning and segmentation guidance. Segment instead of raising it.\n- **Bands.** `icp_fit_score` returns `band` (high / medium / low), a 0-100 `score` and one `evidence` sentence; borderline accounts land `medium`, never a generous `high`.\n- **Widen bar.** Widen only when a segment's sample clears the bar you wrote down; the recipe's default is high + medium above 50%. Plausible-fit on page 1 runs 60-80% for a sharp ICP, under ~40% means loose filters — both carry `benchmark_basis: estimate`, so they calibrate, they do not measure.\n- **Scope defaults.** Dedupe `--scan-limit` is 50,000 rows (cap 200,000), and standing auto-dedupe runs **before** auto-run so enrichment is never queued for rows about to be deleted. `columns run --limit` defaults to 10, inline deterministic runs cap at 25, `--all` requires `--background`. Personas per account have no default — set `--max-per-company` deliberately. Capacity follows the plan size: rows per Table and per workspace are in `oxygen limits show --json` (`storage_capacity`); at a limit only new rows are refused.\n- **Every `--max-credits <cap>` comes from the immediately preceding preview**, route estimate or `recommended_max_credits`. A too-low cap on a waterfall does not stop the run: the expensive lane is refused on its own and the cascade advances, so a cheaper lane still bills while the good one is skipped. Premium managed lanes stay off unless `--allow-premium-lanes` is passed; read a lane's price from `oxygen tools get <tool_id> --json`, never from prose.\n- **`domain` is the account identity, `linkedin_url` the person identity** — both are the upsert keys, neither is overwritten, and provenance accumulates in `source`.\n\n## Accepted-account gate spec\n\nAn account reaches persona sourcing only when `icp_fit_score.band` is `high`, or `medium` **with** an evidence string naming a verifiable fact; `score` is at or above the band floor written into the account-map page; `domain` is present, canonical, and not a directory, aggregator or search URL; the row is not excluded by the ICP spec (competitor, current customer, wrong geography, wrong size); and the row survived dedupe.\n\nEnforcement is a formula column, not prose. Validate it free, attach it, then materialise it — formula values compute when read, so a filter on one is refused by default:\n\n```bash\noxygen formulas validate <accounts> --expression 'if(and(or(path(icp_fit_score, \"band\") == \"high\", path(icp_fit_score, \"band\") == \"medium\"), is_blank(excluded_reason)), \"accepted\", \"held\")' --rows 5 --json\noxygen columns add <accounts> --kind formula --key account_accepted --label \"Accepted\" --definition-json '<validated expression>' --json\noxygen columns run <accounts> account_accepted --force --json\noxygen tables query <accounts> --filter-json '{\"column\":\"account_accepted\",\"op\":\"eq\",\"value\":\"accepted\"}' --formula-values materialized --fields domain --limit 1000 --json\noxygen tables views create <accounts> --name \"Accepted accounts\" --json\n```\n\nNothing bypasses this: never hand the whole accounts table to `oxygen people search run`, never source people at an account whose `icp_fit_score` cell is empty or errored, and never promote a `held` row because the market looks thin. A thin accepted set is a filter or ICP problem, fixed upstream.\n\n## Approval gates, mapped to who decides\n\n| Gate | What it authorises | Attended Copilot | Unattended run |\n| --- | --- | --- | --- |\n| `kit-apply` | 0-credit install of the accounts table and scoring column | auto-approvable workspace write, one card | installs only; arms nothing |\n| `source-sample` | one live company-search page under a cap | human card — provider spend | its own approved per-delivery ceiling |\n| dedupe `--apply --approved` | deleting duplicates (recoverable from row history) | auto-approvable workspace write | standing auto-dedupe, no per-write approval |\n| `fill-firmographics` | one capped company-enrichment run | human card | its own ceiling |\n| `score-accounts` | one paid AI column run under a cap | human card for the cap | its own ceiling; `tables auto-run set` is scoped standing permission for the listed columns |\n| `widen-coverage` | one more segment, re-granted every time | human card, per segment | never standing — a widen is a new purchase |\n| `source-personas` | one live people-search run scoped to accepted domains | human card | its own ceiling |\n| `tables promote --approved` | writing table columns onto matched CRM records | auto-approvable internal truth write | free, still explicit |\n| `outbound-enrollment` | contacting anyone at all | owned by the outbound motion | never granted here |\n\nReads — `recipes show`, both `search plan` commands, `tables query`, `cells inspect`, `lead-sourcing audit`, `budget list`, `limits show` — need no approval and spend nothing. Canonical wiki edits file a proposal a human approves.\n\n## Failure modes\n\n- **Free-text keyword where the provider wants an enum** → `invalid_provider_enum_value`, or worse a noisy page. Fetch the catalog and pass the exact string; free text only tightens an already enum-grounded segment.\n- **A dropped filter read as applied, or an invented market size.** `filter_application[]` reports applied vs dropped per route with the reason; quote `estimated_match_count` only when its `basis` is `provider_count` — any other basis is derived from your requested target, not from the market.\n- **Scoring the whole table before reading page 1** — the failure this motion exists to prevent: it spends several times over before anyone knows the filters work.\n- **Filtering a formula column.** `tables query` refuses formula filters by default because displayed formulas evaluate live. Run with `--force` (free), then `--formula-values materialized`.\n- **Appending instead of merging.** A source run without `--upsert-key domain` re-adds the same companies every page; collapse them, then arm the standing auto-dedupe. Enriching before deduping pays twice for one company.\n- **A benched or unknown managed provider.** `provider_availability` carries `degraded: true` or `availability: \"unknown\"`, and a live run refuses a benched managed primary before table creation. Read each entry's `next_action` — funding clears neither a staff hold nor a rejected key, and an unknown snapshot is re-previewed, not assumed.\n- **A preview-only people route treated as runnable.** Apollo and ContactOut people search return masked records without the stable `linkedin_url` the Contacts upsert contract needs, so `recommended_live_route_id` can name the right contract without naming a runnable route. Confirm `tool_access` is `runnable`, and never accept a route that drops `company.domains` — that is the gate leaking.\n- **An AI column asked a web question.** AI columns have no web access and answer from the row; web answers belong in a `--kind research` column with a `--research-query`. And a `queued` / `not_started` run has captured nothing yet — `oxygen table-runs cancel <run_id> --json` before pickup leaves spend at 0.\n\n## Data-quality checks (after every sourcing cycle)\n\n1. `oxygen tables query <accounts> --limit 100 --json` — rows with a blank `domain`, or one that is a directory, aggregator or search URL. Clear those cells **and** the row's `icp_fit_score` before re-scoring; poisoned cells are not re-scored on their own.\n2. `oxygen tables dedupe <accounts> --on domain --normalize domain --json` — zero groups once auto-dedupe is armed; anything else means a write path bypassed the key.\n3. `oxygen lead-sourcing audit <accounts> --spec ./icp-spec.json --json` — a reason on every exclusion; an exclusion with no reason is a filter you cannot defend.\n4. `oxygen cells inspect <accounts> <row_id> icp_fit_score --json` on one high, one medium and one low row; evidence citing nothing checkable means a thin ICP page.\n5. `oxygen tables query <contacts> --limit 100 --json` — contacts per accepted account; zero is a persona-filter problem, not a market problem. `oxygen table-runs provider-summary <run_id> --json` says which provider answered and at what cost per row.\n\n## Learning loop\n\n- **What auto-files:** the ingestion run and its item outputs, per-cell provider and cost provenance, the knowledge log line each kit apply writes, `table-runs provider-summary`, and the audit output.\n- **What to synthesise monthly:** high-fit share by segment; contacts per accepted account; credits per *accepted account* (not per sourced row — the denominator that matters is the account someone would work); and, once the outbound companion has run, reply rate by segment joined back to the map.\n- **What to change, one rule per loop:** one filter, one band floor, one excluded segment, or one persona title set. Write the change and its reason into the account-map page; canonical positioning files a proposal instead.\n- **\"Good\", written before the first live run:** an accepted account is one with a reachable buyer persona in a segment where you can name a reason you win. Coverage without that definition is a row count, not a market map.\n\n## Go-live checklist\n\n1. `oxygen knowledge page get icp --json` and `oxygen knowledge page get offers --json` — both `active`, the ICP sentence carrying an explicit EXCLUDE clause. (`oxygen knowledge status --json` describes the local CLI mirror and can report zero pages while the workspace is filled; it is not the readiness check.)\n2. `oxygen integrations list --json` and `oxygen billing balance --json` — a company-search provider reachable, headroom for the sample.\n3. `oxygen tools enums get blitzapi industry --query <segment> --json`, then `oxygen companies search plan --prompt \"<ICP sentence>\" --filters-json '<typed filters>' --estimate --json`; read `filter_application` and `provider_availability`, save the plan to a file.\n4. `oxygen recipes apply icp-to-account-map --dry-run --json`, then apply — table and `icp_fit_score` installed, nothing armed.\n5. `source-sample` granted; `oxygen table-ingestions wait <ingestion_run_id> --json` green; 25 rows readable.\n6. Dedupe applied and `oxygen tables auto-dedupe set <accounts> --on domain --normalize domain --json` armed; `fill-firmographics` and `score-accounts` granted; one high, one medium and one low read by hand.\n7. `account_accepted` validated, attached, materialised; `oxygen tables views create <accounts> --name \"Accepted accounts\" --json`.\n8. Widen bar written down; `widen-coverage` granted for the first segment only.\n9. `oxygen blueprints apply contact-finding --json`; `source-personas` for one persona set at accepted domains; `oxygen tables link <contacts> --to <accounts> --on company_domain --json` previewed, then `--approved`.\n10. `oxygen budget list --json` and `oxygen limits show --json` — org backstop and capacity headroom in place; `oxygen knowledge page upsert --slug account-map --type research_note --json` records segments, filters, band floor and widen decisions.\n\n## Open questions (state them, do not resolve them)\n\n- Company search publishes no `schedulable` field the way signal search does, so a recurring refresh is discovered by attempting `oxygen feeds bind <accounts> --kind company_search --upsert-key domain --every daily@9 --max-credits <cap> --approved --json` and treating a `feed_not_incremental` refusal as the answer. Until then the supported cadence is a monthly re-plan through the same gates.\n- The recipe's fit bands are `benchmark_basis: estimate` with no workspace-measured baseline behind them, and the canonical ICP scorer function is not yet ratifiable — this playbook uses the kit's table AI column, never a callable.\n- Whether accepted accounts should become CRM company records at map time is unratified. This playbook keeps the map in Tables and promotes only accounts a human is working; a CRM *person* is created at the warm-lead gate downstream, never at sourcing time.\n- Cross-table suppression is read-only through `oxygen tables dedupe <accounts> --on domain --against <suppression-table> --against-column domain --json`; no standing suppression policy exists on a source run, so exclusions live in the plan's EXCLUDE clause and the audit spec.\n\n## Resume point\n\n`oxygen recipes show icp-to-account-map --json`. Its `kit` block names each stage's install status; `oxygen tables list --json` says whether an accounts or contacts table already exists, and `oxygen tables auto-dedupe get <accounts> --json` plus `oxygen tables auto-run get <accounts> --json` say what already runs on writes. Start there, never from scratch.\n\n## What the human still has to do\n\nNothing above contacts anyone, and nothing widens on its own. The human writes the ICP sentence and band floor into `oxygen knowledge page upsert --slug account-map --json`; grants `source-sample`, `fill-firmographics` and `score-accounts`, each with a cap read from its own preview; reads one high, one medium and one low row with `oxygen cells inspect <accounts> <row_id> icp_fit_score --json` and decides whether the filters or the ICP page need fixing; grants `widen-coverage` once per segment; grants `source-personas` after choosing the persona titles and `--max-per-company`; and decides whether the accepted map goes to `outbound-pilot-50` now or after another coverage pass. That read is the step routinely skipped, and the one the whole motion is built around.\n",
|
|
19
19
|
},
|
|
20
20
|
{
|
|
21
21
|
slug: "linkedin-content-strategy",
|
|
22
22
|
title: "Playbook: LinkedIn content strategy",
|
|
23
23
|
sources: ["oxygen-playbooks/playbooks/linkedin-content-strategy.md"],
|
|
24
|
-
sha256: "
|
|
25
|
-
content: "---\nname: linkedin-content-strategy\ndescription: \"Install and run the founder LinkedIn content engine: a wiki-grounded strategy page, an idea backlog, a week of drafts approved post by post, scheduled publishing, answered comments, weekly engagement read-back, outlier mining, and the hand-off of engagers to inbound-led outbound.\"\n---\n\n# Playbook: LinkedIn content strategy\n\n## Motion in one sentence\n\nA strategy page written from the wiki fixes the pillars, cadence and voice; ideas accumulate in a backlog; a week of drafts is generated from that wiki and approved one post at a time; the scheduler publishes each from the founder's own account; comments are answered through a previewed reply; engagement is read back weekly; outlier mining says which structures travelled; the lessons file back into the wiki; and the engagers become the inbound signal. Each stage reads the page the last one wrote — a posting habit without the strategy page is a treadmill, and an outlier bank without the habit is a swipe file nobody drains.\n\nRead first: `oxygen recipes show founder-posting-system --json`, `oxygen recipes show weekly-content-calendar --json`, `oxygen recipes show content-outlier-mining --json` (the stage 7 kit). Mechanics: `oxygen skills get oxygen-linkedin-marketing --json`.\n\n## Stage table\n\n| # | Stage | Owner | Install / command | Inputs | Cap | Approval | Verify | Stop |\n| --- | --- | --- | --- | --- | --- | --- | --- | --- |\n| 0 | Context + strategy | Knowledge Graph | `oxygen knowledge resolve --purpose outbound_copy --require-ready --json` → `oxygen knowledge page upsert --slug content-strategy --type strategy --title \"Content strategy\" --status active --body \"<pillars / cadence / voice>\" --json` | `positioning`, `icp`, `offers`, `voice`, all `active` | 3–5 pillars, each naming one ICP problem | none — a `strategy` page is working knowledge, logged as a revision | `oxygen knowledge page get content-strategy --json` | a stub ICP page, or a pillar with no buyer problem |\n| 1 | Sender | Publishing | `oxygen senders list --status active --json`; `oxygen senders health <sender_id> --json` (`oxygen senders limits get` for the budgets) | one connected LinkedIn account | own-feed ceiling 25 posts / 24h | none — reads | `active`, no open checkpoint | `restricted` / `credentials_required`: reconnect, never route around it |\n| 2 | Backlog | Publishing | `oxygen publishing ideas add --text \"<angle>\" --topic <pillar> --json`; `oxygen publishing ideas list --json` | angles as they occur; structures from stage 7 | two weeks of slots ahead | none (internal write) | `oxygen publishing ideas list --limit 50 --json` | an empty backlog on drafting day: mine outliers, don't draft from taste |\n| 3 | Draft + queue | Publishing | `oxygen publishing posts draft --template personal_story --topic \"<angle>\" --max-credits <cap> --json` → `oxygen publishing drafts edit <draft_id> --text-file mon.txt --json` → `oxygen publishing drafts accept <draft_id> --publish-at <iso+offset> --sender <sender_id> --json` | a pillar + a backlog angle per slot | 3–5 posts/week; 1–5 variants (default 3) | `draft-week`: paid, cap from the preview; queueing needs none — it never sends | `oxygen publishing posts list --approval-status needs_approval --json` | a bare `--publish-at`: with no offset it is stored as UTC |\n| 4 | Review + publish | Publishing | `oxygen publishing posts review <post_id> --max-credits <cap> --json`; `oxygen publishing mentions resolve --text-file mon.txt --json` → `oxygen publishing posts approve <post_id> --json` | the exact final text, read by the founder | ≤15% promotion posts; ≤1 lead-magnet CTA/week | `review-post`: paid, advisory. `post-publish`: human, every post, never batched | `oxygen publishing posts get <post_id> --json` — attempts, provider id, deep-link | an unresolved `@<public-identifier>`; `linkedin_rate_limited` |\n| 5 | Comments | Publishing | `oxygen publishing comments list --view unanswered --json` → `oxygen publishing comments reply <comment_id> --text-file reply.txt --json` → `oxygen publishing comments approve <action_id> --content-hash <sha256> --approved --json` → `oxygen publishing comments resolve <comment_id> --json` | the unanswered queue (30-day scope) | one reply per comment, inside 48h | `comment-reply`: human approves that exact previewed text and its hash | the queue drains; handled items read `resolved` | a comment older than 48h with no reply |\n| 6 | Read back | Publishing + Posts | `oxygen publishing analytics summary --channel linkedin --range 30d --json` (winners, `leader` lead, `null_reasons`); `oxygen publishing analytics timeseries --channel linkedin --range 30d --json` (earned per day, growing or shrinking vs the previous period, follower change); `oxygen publishing analytics post <post_id> --json`; `oxygen posts get --post <post_url> --json` | published posts | metrics refresh ~30 days after publish | none — reads | reactions, comments, reshares and — on your own original posts — impressions present; reposts counted in `reposts_excluded`, not in totals | every metric zero after two days: confirm `published`, not `deferred` |\n| 7 | Mine outliers | Workflows — kit stage `linkedin-keyword-outliers` | `oxygen recipes apply content-outlier-mining --dry-run --json` → without `--dry-run`; then `oxygen blueprints apply linkedin-creator-outliers --table-ref content_outliers=<bank_table_id> --json` | 3–4 buyer-language keywords; ≤25 public `/in/` URLs | 7-day lookback; 1 page/keyword on the pilot; ceiling from preflight | `pilot-live` then `sweep-arm`: human, re-granted on reapply | `oxygen workflows tail <run_id> --json`; `oxygen tables query <bank_table_id> --json` | everything near par, or top rows are hiring posts |\n| 8 | Learn | Knowledge Graph | `oxygen knowledge synthesize --kind voice --json` → `--approved`; `oxygen knowledge log append --event decision --slug content-strategy --json` | analytics, comments, the bank, draft edits | one rule per loop | canonical voice / positioning file a proposal: `oxygen knowledge proposals approve <id> --json` | `oxygen knowledge page get content-strategy --json` | a change with no receipt behind it |\n| 9 | Hand off | Signals → inbound-led outbound | `oxygen engagement harvest --post <social_id> --source unipile --recurring --json`; `oxygen engagement status --post <social_id> --json` | the composite `social_id` from `oxygen posts get` | free; drips against the ingest budget | none — it contacts nobody | `oxygen engagement engagers --post <social_id> --json` previews the table | outreach begins here: run the inbound-led-outbound playbook |\n\n## Numeric guardrails\n\n- **Cadence and mix:** 3–5 posts/week planned; 2–3/week for four consecutive weeks is the floor before any signal is readable. At most 15% promotion posts and one lead-magnet CTA post per week. Consistency leads, reach lags.\n- **Own-feed ceiling 25 posts / 24h** per account, a platform default: own-feed publishing sits outside the outreach send quotas because it is not aimed at another member, but still refuses a sender that is not `active`. Own-post reads draw the other-API-read budget (200/day default, 1,000 max); `engagement harvest` draws the separate ingest budget (20/day, 200 max), so a big post drips over days. `oxygen engagement engagers` walks both sources up to `--max-pages` (default 5, max 20) × 100.\n- **Paid calls:** 1–5 variants per draft call (default 3), one AI call per draft and per review. Take every ceiling from the preview and `oxygen tools get <tool_id> --json`, never from prose; sweep ceilings from `oxygen blueprints preflight`.\n- **Comment SLA 48 hours**, over a rolling 30-day owned-post scope: an older thread is absent from the queue, not silent. Analytics refresh for ~30 days; reactions, comments and reshares are real; impressions come from LinkedIn's own post analytics, which it shows only to the author, so they are real for the connected member's original posts; saves and sends are not exposed.\n- **Outlier scoring:** `rank_score = 100 × √(outlier_multiple × audience_index)`, **100 is par**; a healthy sweep puts ~5–15% above 200. Defaults: `lookback_days` 7, `min_post_age_hours` 24, `min_author_followers` 1000, weights 1 / 3 / 5; 3–4 keywords (max 8), 10–25 creators (max 25).\n\n## Angle gate spec\n\nThe gate is not who gets contacted — nobody is contacted here — it is **which idea becomes a public post in the founder's name**. An item advances to draft only when all of these hold: it maps to a named pillar on `[[content-strategy]]`; it carries one concrete claim, story or artifact traceable to a wiki page, a shipped thing or a real customer outcome; it names the audience in plain words; if promotional, the week's share is still under 15%; and its voice matches the pinned voice page. The first three are enforced at drafting, because the draft call grounds on the wiki by default; voice by the free channel lint plus the paid `publishing posts review` check.\n\nNothing bypasses it, because **approval is per post and is not batchable server-side**: a draft accepted from the AI queue, a post created directly, and a row loaded by `oxygen publishing import` all land needs-approval. Borrowing from the bank has its own rule — lift the **structure**, never the text, and only from a row that cleared `rank_score` 200 with a `baseline_kind` you trust; `author_unresolved` and `maturing` rows are unscored and are not evidence.\n\n## Approval gates, mapped to who decides\n\n| Gate | What it authorises | Copilot posture |\n| --- | --- | --- |\n| kit apply (`oxygen recipes apply content-outlier-mining`) | 0-credit install of the sweep, workflow disabled | human card, once |\n| `draft-week` / `review-post` | one paid AI drafting or voice/claims call | human sets each cap from the preview |\n| `post-publish` | one public post in the founder's name, on this exact text | human, every post, never batched |\n| `comment-reply` | one public reply bound to the preview's `action_id` and `content_hash` | human approves that exact text |\n| `pilot-live` then `sweep-arm` | one live outlier cycle, then the recurring sweep | human for each; re-grant on reapply (revision-bound) |\n| amplification (`oxygen publishing amplification create`, then `enable`) | real public engagement from teammates' accounts, spending credits; LinkedIn may restrict those accounts | human twice: grant, then arm, each with `--acknowledge-risk` |\n| canonical wiki edits | voice / brand / positioning / pinned playbooks | a proposal a human approves |\n\nInstallation is never permission: the kit applies at 0 credits with its workflow disabled, and every paid run, publish, reply and armed sweep is its own gate above.\n\nReads need no approval (`oxygen publishing posts list`, `oxygen publishing analytics summary`, `oxygen senders health`, `oxygen tables query`); working wiki pages write as logged revisions.\n\n## Failure modes\n\n- **The two axes.** `--status` is pipeline position (`draft` → `scheduled` → `queued` → `published`); `--approval-status` is whether a human said yes. A queued post is both `scheduled` and `needs_approval`. Filter on the approval axis or you will report an empty queue you just filled.\n- **A bare local `--publish-at`.** With no offset it is stored as UTC; `--timezone` only changes the display. Pass an offset or `Z`.\n- **The activity URN is not the `social_id`.** `posts reactions`, `posts comments`, `engagement engagers` and `engagement harvest` all need the composite `social_id` from `oxygen posts get`. Nothing lists your own feed, so record the id when the post goes out.\n- **Mentions and content.** A verified `@<public-identifier>` goes in the post text — a structured `mentions` array is ignored and `content.mentions` rejected; approve blocks anything unresolved. `--content-json` replaces the whole content object, so a media update drops an existing `first_comment` unless you resend it.\n- **A post on a dead sender never dispatches**; fix the account instead of re-queueing. A rate limit **defers** rather than drops and names `resets_at`, so retrying in a loop only burns quota. Company-page posting is a tested-negative anti-pattern — post from the personal profile.\n- **Generic drafts are a thin wiki, not a thin prompt**; a bank where everything scores near par is a keyword naming a category. Fix the page or the phrase, never the prompt length or the weights.\n- **Outreach leaking in.** The moment the plan is to message an engager, this motion is over — hand off at stage 9. Sequences own initiation; a CRM person is created at a warm-lead gate, never because someone reacted.\n\n## Data-quality checks (after every week)\n\n1. `oxygen publishing posts list --status published --json` — every planned slot published, or one sat `needs_approval` all week? A missed slot is the failure this motion exists to prevent.\n2. `oxygen publishing comments list --view unanswered --json` — nothing older than 48h, and never `resolved` on a thread still owed an answer.\n3. `oxygen publishing analytics summary --channel linkedin --range 30d --json` — all-zero after two days means `deferred`. Then read the comments, not the counts: ICP titles, or peers and recruiters?\n4. `oxygen tables query <bank_table_id> --limit 25 --json` — mostly `author_unresolved` or `maturing` is a cold cache or short lookback; a backlog under two weeks deep means next week starts from taste.\n\n## Learning loop\n\n- **What auto-files:** a draft edit snapshots the AI's original copy, so an accepted edit records what the founder changed; `oxygen publishing drafts reject <draft_id> --reason \"...\" --json` files the reason; revisions, review findings, the metric series and the comment queue are durable.\n- **What to synthesise weekly:** which pillar produced ICP-fit comments rather than peer likes; which post *shape* travelled (a number in line one, a named enemy, a before/after); and `oxygen knowledge synthesize --kind voice --json`, a voice guide distilled from real sent copy.\n- **What to change, one rule per loop:** the pillar mix, a slot's time, a keyword on the source table (pause it, never delete it), the CTA, or the voice page. Write the change and its reason into the wiki; a voice or positioning change files a proposal.\n- **Define \"good\" first:** an inbound conversation from an ICP-fit person — a DM, a comment thread that becomes a call, a reply naming their own version of the problem. Not reach, not reactions, not a bank with more rows.\n\n## Go-live checklist\n\n1. `oxygen senders list --status active --json`, then `oxygen senders health <sender_id> --json` — one healthy account, no open checkpoint.\n2. `oxygen knowledge resolve --purpose outbound_copy --require-ready --json`, then `oxygen knowledge page upsert --slug content-strategy --type strategy --status active --body \"...\" --json`.\n3. `oxygen publishing ideas add --text \"...\" --topic <pillar> --json` until two weeks of slots exist, then draft and queue each slot (stage 3).\n4. `oxygen publishing posts list --approval-status needs_approval --json`, present the week, then `oxygen publishing posts approve <post_id> --json` one post at a time.\n5. `oxygen publishing comments list --view unanswered --json` — the 48h loop runs and a named person owns it.\n6. `oxygen recipes apply content-outlier-mining --dry-run --json`, apply, one `oxygen workflows call <workflow_id> --mode dry-run --json`, one `pilot-live` cycle, then `oxygen workflows enable <workflow_id> --approved --max-credits <cap> --json`.\n7. `oxygen budget list --json` for the org backstop, then `oxygen knowledge log append --event note --slug content-strategy --json`.\n\n## Open questions (state them, do not resolve them)\n\n- LinkedIn shows impressions only to a post's author and never exposes saves or sends. Reach is measurable for the connected member's original posts (and earned-media value once a CPM is set with `oxygen publishing analytics emv`); for anything else it stays empty. Never substitute a proxy and call it reach.\n- The cadence and mix numbers are operator estimates, not measured benchmarks. Replace them with four weeks of your own evidence.\n- The outlier kit is `beta`, and `oxygen recipes apply content-outlier-mining` installs only the keyword sweep; the creator watch applies separately against the same tables with `--table-ref`, and omitting those flags reports a table collision — the guard working.\n- Whether engagers flow automatically into outreach is not this playbook's call; stage 9 hands them over deliberately. Amplification from teammates' accounts is likewise a founder decision about the company's public name.\n\n## Resume point\n\n`oxygen recipes show founder-posting-system --json` and `oxygen knowledge page get content-strategy --json`, then `oxygen publishing posts list --json`, `oxygen publishing comments list --json` and `oxygen workflows list --json`. Start from what exists, never from scratch.\n",
|
|
24
|
+
sha256: "4b182957e72e46be3ff79cf0af68ad073814c3fd88c69289c2d54aa238f1735a",
|
|
25
|
+
content: "---\nname: linkedin-content-strategy\ndescription: \"Install and run the founder LinkedIn content engine: a wiki-grounded strategy page, an idea backlog, a week of drafts approved post by post, scheduled publishing, answered comments, weekly engagement read-back, outlier mining, and the hand-off of engagers to inbound-led outbound.\"\n---\n\n# Playbook: LinkedIn content strategy\n\n## Motion in one sentence\n\nA strategy page written from the wiki fixes the pillars, cadence and voice; ideas accumulate in a backlog; a week of drafts is generated from that wiki and approved one post at a time; the scheduler publishes each from the founder's own account; comments are answered through a previewed reply; engagement is read back weekly; outlier mining says which structures travelled; the lessons file back into the wiki; and the engagers become the inbound signal. Each stage reads the page the last one wrote — a posting habit without the strategy page is a treadmill, and an outlier bank without the habit is a swipe file nobody drains.\n\nRead first: `oxygen recipes show founder-posting-system --json`, `oxygen recipes show weekly-content-calendar --json`, `oxygen recipes show content-outlier-mining --json` (the stage 7 kit). Mechanics: `oxygen skills get oxygen-linkedin-marketing --json`.\n\n## Stage table\n\n| # | Stage | Owner | Install / command | Inputs | Cap | Approval | Verify | Stop |\n| --- | --- | --- | --- | --- | --- | --- | --- | --- |\n| 0 | Context + strategy | Knowledge Graph | `oxygen knowledge resolve --purpose outbound_copy --require-ready --json` → `oxygen knowledge page upsert --slug content-strategy --type strategy --title \"Content strategy\" --status active --body \"<pillars / cadence / voice>\" --json` | `positioning`, `icp`, `offers`, `voice`, all `active` | 3–5 pillars, each naming one ICP problem | none — a `strategy` page is working knowledge, logged as a revision | `oxygen knowledge page get content-strategy --json` | a stub ICP page, or a pillar with no buyer problem |\n| 1 | Sender | Publishing | `oxygen senders list --status active --json`; `oxygen senders health <sender_id> --json` (`oxygen senders limits get` for the budgets) | one connected LinkedIn account | `posts_per_day` (default 10, max 25) | none — reads | `active`, no open checkpoint | `restricted` / `credentials_required`: reconnect, never route around it |\n| 2 | Backlog | Publishing | `oxygen publishing ideas add --text \"<angle>\" --topic <pillar> --json`; `oxygen publishing ideas list --json` | angles as they occur; structures from stage 7 | two weeks of slots ahead | none (internal write) | `oxygen publishing ideas list --limit 50 --json` | an empty backlog on drafting day: mine outliers, don't draft from taste |\n| 3 | Draft + queue | Publishing | `oxygen publishing posts draft --template personal_story --topic \"<angle>\" --max-credits <cap> --json` → `oxygen publishing drafts edit <draft_id> --text-file mon.txt --json` → `oxygen publishing drafts accept <draft_id> --publish-at <iso+offset> --sender <sender_id> --json` | a pillar + a backlog angle per slot | 3–5 posts/week; 1–5 variants (default 3) | `draft-week`: paid, cap from the preview; queueing needs none — it never sends | `oxygen publishing posts list --approval-status needs_approval --json` | a bare `--publish-at`: with no offset it is stored as UTC |\n| 4 | Review + publish | Publishing | `oxygen publishing posts review <post_id> --max-credits <cap> --json`; `oxygen publishing mentions resolve --text-file mon.txt --json` → `oxygen publishing posts approve <post_id> --json` | the exact final text, read by the founder | ≤15% promotion posts; ≤1 lead-magnet CTA/week | `review-post`: paid, advisory. `post-publish`: human, every post, never batched | `oxygen publishing posts get <post_id> --json` — attempts, provider id, deep-link | an unresolved `@<public-identifier>`; `linkedin_rate_limited` |\n| 5 | Comments | Publishing | `oxygen publishing comments list --view unanswered --json` → `oxygen publishing comments reply <comment_id> --text-file reply.txt --json` → `oxygen publishing comments approve <action_id> --content-hash <sha256> --approved --json` → `oxygen publishing comments resolve <comment_id> --json` | the unanswered queue (30-day scope) | one reply per comment, inside 48h | `comment-reply`: human approves that exact previewed text and its hash | the queue drains; handled items read `resolved` | a comment older than 48h with no reply |\n| 6 | Read back | Publishing + Posts | `oxygen publishing analytics summary --channel linkedin --range 30d --json` (winners, `leader` lead, `null_reasons`); `oxygen publishing analytics timeseries --channel linkedin --range 30d --json` (earned per day, growing or shrinking vs the previous period, follower change); `oxygen publishing analytics post <post_id> --json`; `oxygen posts get --post <post_url> --json` | published posts | metrics refresh ~30 days after publish | none — reads | reactions, comments, reshares and — on your own original posts — impressions present; reposts counted in `reposts_excluded`, not in totals | every metric zero after two days: confirm `published`, not `deferred` |\n| 7 | Mine outliers | Workflows — kit stage `linkedin-keyword-outliers` | `oxygen recipes apply content-outlier-mining --dry-run --json` → without `--dry-run`; then `oxygen blueprints apply linkedin-creator-outliers --table-ref content_outliers=<bank_table_id> --json` | 3–4 buyer-language keywords; ≤25 public `/in/` URLs | 7-day lookback; 1 page/keyword on the pilot; ceiling from preflight | `pilot-live` then `sweep-arm`: human, re-granted on reapply | `oxygen workflows tail <run_id> --json`; `oxygen tables query <bank_table_id> --json` | everything near par, or top rows are hiring posts |\n| 8 | Learn | Knowledge Graph | `oxygen knowledge synthesize --kind voice --json` → `--approved`; `oxygen knowledge log append --event decision --slug content-strategy --json` | analytics, comments, the bank, draft edits | one rule per loop | canonical voice / positioning file a proposal: `oxygen knowledge proposals approve <id> --json` | `oxygen knowledge page get content-strategy --json` | a change with no receipt behind it |\n| 9 | Hand off | Signals → inbound-led outbound | `oxygen engagement harvest --post <social_id> --source unipile --recurring --json`; `oxygen engagement status --post <social_id> --json` | the composite `social_id` from `oxygen posts get` | free; drips against the ingest budget | none — it contacts nobody | `oxygen engagement engagers --post <social_id> --json` previews the table | outreach begins here: run the inbound-led-outbound playbook |\n\n## Numeric guardrails\n\n- **Cadence and mix:** 3–5 posts/week planned; 2–3/week for four consecutive weeks is the floor before any signal is readable. At most 15% promotion posts and one lead-magnet CTA post per week. Consistency leads, reach lags.\n- **Own-feed cap `posts_per_day`** per account (default 10, max 25): own-feed publishing sits outside the outreach send quotas because it is not aimed at another member, but still refuses a sender that is not `active`. Own-post reads draw the other-API-read budget (200/day default, 1,000 max); `engagement harvest` draws the separate ingest budget (20/day, 200 max), so a big post drips over days. `oxygen engagement engagers` walks both sources up to `--max-pages` (default 5, max 20) × 100.\n- **Paid calls:** 1–5 variants per draft call (default 3), one AI call per draft and per review. Take every ceiling from the preview and `oxygen tools get <tool_id> --json`, never from prose; sweep ceilings from `oxygen blueprints preflight`.\n- **Comment SLA 48 hours**, over a rolling 30-day owned-post scope: an older thread is absent from the queue, not silent. Analytics refresh for ~30 days; reactions, comments and reshares are real; impressions come from LinkedIn's own post analytics, which it shows only to the author, so they are real for the connected member's original posts; saves and sends are not exposed.\n- **Outlier scoring:** `rank_score = 100 × √(outlier_multiple × audience_index)`, **100 is par**; a healthy sweep puts ~5–15% above 200. Defaults: `lookback_days` 7, `min_post_age_hours` 24, `min_author_followers` 1000, weights 1 / 3 / 5; 3–4 keywords (max 8), 10–25 creators (max 25).\n\n## Angle gate spec\n\nThe gate is not who gets contacted — nobody is contacted here — it is **which idea becomes a public post in the founder's name**. An item advances to draft only when all of these hold: it maps to a named pillar on `[[content-strategy]]`; it carries one concrete claim, story or artifact traceable to a wiki page, a shipped thing or a real customer outcome; it names the audience in plain words; if promotional, the week's share is still under 15%; and its voice matches the pinned voice page. The first three are enforced at drafting, because the draft call grounds on the wiki by default; voice by the free channel lint plus the paid `publishing posts review` check.\n\nNothing bypasses it, because **approval is per post and is not batchable server-side**: a draft accepted from the AI queue, a post created directly, and a row loaded by `oxygen publishing import` all land needs-approval. Borrowing from the bank has its own rule — lift the **structure**, never the text, and only from a row that cleared `rank_score` 200 with a `baseline_kind` you trust; `author_unresolved` and `maturing` rows are unscored and are not evidence.\n\n## Approval gates, mapped to who decides\n\n| Gate | What it authorises | Copilot posture |\n| --- | --- | --- |\n| kit apply (`oxygen recipes apply content-outlier-mining`) | 0-credit install of the sweep, workflow disabled | human card, once |\n| `draft-week` / `review-post` | one paid AI drafting or voice/claims call | human sets each cap from the preview |\n| `post-publish` | one public post in the founder's name, on this exact text | human, every post, never batched |\n| `comment-reply` | one public reply bound to the preview's `action_id` and `content_hash` | human approves that exact text |\n| `pilot-live` then `sweep-arm` | one live outlier cycle, then the recurring sweep | human for each; re-grant on reapply (revision-bound) |\n| amplification (`oxygen publishing amplification create`, then `enable`) | real public engagement from teammates' accounts, spending credits; LinkedIn may restrict those accounts | human twice: grant, then arm, each with `--acknowledge-risk` |\n| canonical wiki edits | voice / brand / positioning / pinned playbooks | a proposal a human approves |\n\nInstallation is never permission: the kit applies at 0 credits with its workflow disabled, and every paid run, publish, reply and armed sweep is its own gate above.\n\nReads need no approval (`oxygen publishing posts list`, `oxygen publishing analytics summary`, `oxygen senders health`, `oxygen tables query`); working wiki pages write as logged revisions.\n\n## Failure modes\n\n- **The two axes.** `--status` is pipeline position (`draft` → `scheduled` → `queued` → `published`); `--approval-status` is whether a human said yes. A queued post is both `scheduled` and `needs_approval`. Filter on the approval axis or you will report an empty queue you just filled.\n- **A bare local `--publish-at`.** With no offset it is stored as UTC; `--timezone` only changes the display. Pass an offset or `Z`.\n- **The activity URN is not the `social_id`.** `posts reactions`, `posts comments`, `engagement engagers` and `engagement harvest` all need the composite `social_id` from `oxygen posts get`. Nothing lists your own feed, so record the id when the post goes out.\n- **Mentions and content.** A verified `@<public-identifier>` goes in the post text — a structured `mentions` array is ignored and `content.mentions` rejected; approve blocks anything unresolved. `--content-json` replaces the whole content object, so a media update drops an existing `first_comment` unless you resend it.\n- **A post on a dead sender never dispatches**; fix the account instead of re-queueing. A rate limit **defers** rather than drops and names `resets_at`, so retrying in a loop only burns quota. Company-page posting is a tested-negative anti-pattern — post from the personal profile.\n- **Generic drafts are a thin wiki, not a thin prompt**; a bank where everything scores near par is a keyword naming a category. Fix the page or the phrase, never the prompt length or the weights.\n- **Outreach leaking in.** The moment the plan is to message an engager, this motion is over — hand off at stage 9. Sequences own initiation; a CRM person is created at a warm-lead gate, never because someone reacted.\n\n## Data-quality checks (after every week)\n\n1. `oxygen publishing posts list --status published --json` — every planned slot published, or one sat `needs_approval` all week? A missed slot is the failure this motion exists to prevent.\n2. `oxygen publishing comments list --view unanswered --json` — nothing older than 48h, and never `resolved` on a thread still owed an answer.\n3. `oxygen publishing analytics summary --channel linkedin --range 30d --json` — all-zero after two days means `deferred`. Then read the comments, not the counts: ICP titles, or peers and recruiters?\n4. `oxygen tables query <bank_table_id> --limit 25 --json` — mostly `author_unresolved` or `maturing` is a cold cache or short lookback; a backlog under two weeks deep means next week starts from taste.\n\n## Learning loop\n\n- **What auto-files:** a draft edit snapshots the AI's original copy, so an accepted edit records what the founder changed; `oxygen publishing drafts reject <draft_id> --reason \"...\" --json` files the reason; revisions, review findings, the metric series and the comment queue are durable.\n- **What to synthesise weekly:** which pillar produced ICP-fit comments rather than peer likes; which post *shape* travelled (a number in line one, a named enemy, a before/after); and `oxygen knowledge synthesize --kind voice --json`, a voice guide distilled from real sent copy.\n- **What to change, one rule per loop:** the pillar mix, a slot's time, a keyword on the source table (pause it, never delete it), the CTA, or the voice page. Write the change and its reason into the wiki; a voice or positioning change files a proposal.\n- **Define \"good\" first:** an inbound conversation from an ICP-fit person — a DM, a comment thread that becomes a call, a reply naming their own version of the problem. Not reach, not reactions, not a bank with more rows.\n\n## Go-live checklist\n\n1. `oxygen senders list --status active --json`, then `oxygen senders health <sender_id> --json` — one healthy account, no open checkpoint.\n2. `oxygen knowledge resolve --purpose outbound_copy --require-ready --json`, then `oxygen knowledge page upsert --slug content-strategy --type strategy --status active --body \"...\" --json`.\n3. `oxygen publishing ideas add --text \"...\" --topic <pillar> --json` until two weeks of slots exist, then draft and queue each slot (stage 3).\n4. `oxygen publishing posts list --approval-status needs_approval --json`, present the week, then `oxygen publishing posts approve <post_id> --json` one post at a time.\n5. `oxygen publishing comments list --view unanswered --json` — the 48h loop runs and a named person owns it.\n6. `oxygen recipes apply content-outlier-mining --dry-run --json`, apply, one `oxygen workflows call <workflow_id> --mode dry-run --json`, one `pilot-live` cycle, then `oxygen workflows enable <workflow_id> --approved --max-credits <cap> --json`.\n7. `oxygen budget list --json` for the org backstop, then `oxygen knowledge log append --event note --slug content-strategy --json`.\n\n## Open questions (state them, do not resolve them)\n\n- LinkedIn shows impressions only to a post's author and never exposes saves or sends. Reach is measurable for the connected member's original posts (and earned-media value once a CPM is set with `oxygen publishing analytics emv`); for anything else it stays empty. Never substitute a proxy and call it reach.\n- The cadence and mix numbers are operator estimates, not measured benchmarks. Replace them with four weeks of your own evidence.\n- The outlier kit is `beta`, and `oxygen recipes apply content-outlier-mining` installs only the keyword sweep; the creator watch applies separately against the same tables with `--table-ref`, and omitting those flags reports a table collision — the guard working.\n- Whether engagers flow automatically into outreach is not this playbook's call; stage 9 hands them over deliberately. Amplification from teammates' accounts is likewise a founder decision about the company's public name.\n\n## Resume point\n\n`oxygen recipes show founder-posting-system --json` and `oxygen knowledge page get content-strategy --json`, then `oxygen publishing posts list --json`, `oxygen publishing comments list --json` and `oxygen workflows list --json`. Start from what exists, never from scratch.\n",
|
|
26
26
|
},
|
|
27
27
|
{
|
|
28
28
|
slug: "inbound-led-outbound",
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PURE hard-bounce rule: when a fleet's bounces are a reputation problem rather
|
|
3
|
+
* than list noise.
|
|
4
|
+
*
|
|
5
|
+
* WHY it lives in @oxygen/shared and not next to the other email-health rules in
|
|
6
|
+
* @oxygen/integrations: the tenant rollup applies it per mailbox, and
|
|
7
|
+
* @oxygen/tenant-db must never import @oxygen/integrations (integrations depends
|
|
8
|
+
* on tenant-db). It is re-exported from
|
|
9
|
+
* packages/integrations/src/email-health/health-state.ts, which stays the
|
|
10
|
+
* email-health surface every caller reads.
|
|
11
|
+
*
|
|
12
|
+
* DIRECTIONAL: it produces a warning, never pauses a mailbox or changes a cap.
|
|
13
|
+
*/
|
|
14
|
+
/** Hard-bounce rate above which a fleet is burning its domains, not just its list. */
|
|
15
|
+
export declare const HARD_BOUNCE_RATE_CEILING = 0.03;
|
|
16
|
+
/** Below this send volume a bounce rate is noise, not a signal. */
|
|
17
|
+
export declare const HARD_BOUNCE_RATE_MIN_SENDS = 20;
|
|
18
|
+
/**
|
|
19
|
+
* True when a hard-bounce count is high enough, over enough sends, to be a real
|
|
20
|
+
* reputation problem rather than list noise.
|
|
21
|
+
*/
|
|
22
|
+
export declare function hardBounceRateIsHigh(input: {
|
|
23
|
+
hardBounces: number;
|
|
24
|
+
coldSends: number;
|
|
25
|
+
}): boolean;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PURE hard-bounce rule: when a fleet's bounces are a reputation problem rather
|
|
3
|
+
* than list noise.
|
|
4
|
+
*
|
|
5
|
+
* WHY it lives in @oxygen/shared and not next to the other email-health rules in
|
|
6
|
+
* @oxygen/integrations: the tenant rollup applies it per mailbox, and
|
|
7
|
+
* @oxygen/tenant-db must never import @oxygen/integrations (integrations depends
|
|
8
|
+
* on tenant-db). It is re-exported from
|
|
9
|
+
* packages/integrations/src/email-health/health-state.ts, which stays the
|
|
10
|
+
* email-health surface every caller reads.
|
|
11
|
+
*
|
|
12
|
+
* DIRECTIONAL: it produces a warning, never pauses a mailbox or changes a cap.
|
|
13
|
+
*/
|
|
14
|
+
/** Hard-bounce rate above which a fleet is burning its domains, not just its list. */
|
|
15
|
+
export const HARD_BOUNCE_RATE_CEILING = 0.03;
|
|
16
|
+
/** Below this send volume a bounce rate is noise, not a signal. */
|
|
17
|
+
export const HARD_BOUNCE_RATE_MIN_SENDS = 20;
|
|
18
|
+
/**
|
|
19
|
+
* True when a hard-bounce count is high enough, over enough sends, to be a real
|
|
20
|
+
* reputation problem rather than list noise.
|
|
21
|
+
*/
|
|
22
|
+
export function hardBounceRateIsHigh(input) {
|
|
23
|
+
if (!Number.isFinite(input.coldSends) || input.coldSends < HARD_BOUNCE_RATE_MIN_SENDS) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
return input.hardBounces / input.coldSends > HARD_BOUNCE_RATE_CEILING;
|
|
27
|
+
}
|
|
@@ -71,6 +71,11 @@ export type FeatureGateResolution = {
|
|
|
71
71
|
* - OXYGEN_AGENTS_ENABLED — (b). Authorizes autonomous Agent run execution —
|
|
72
72
|
* tool authority AND credit spend (`apps/web/src/lib/agent-runtime.ts`,
|
|
73
73
|
* `apps/worker/src/agent-cycle.ts`). Same class as publishing.
|
|
74
|
+
* - OXYGEN_REPRICING_2026_09_EFFECTIVE_AT — (b). The instant every held price
|
|
75
|
+
* rise and limit cut of the 2026-09 repricing takes effect
|
|
76
|
+
* (`packages/shared/src/repricing.ts`). A remotely flippable value would let
|
|
77
|
+
* a mis-click move every customer's prices; it changes only through Doppler,
|
|
78
|
+
* with explicit human approval.
|
|
74
79
|
*
|
|
75
80
|
* The list is a guard, not a to-do: none of these is migrated, and
|
|
76
81
|
* `findFeatureGateRegistryViolations` fails the build's tests if one ever is.
|
|
@@ -80,7 +85,7 @@ export declare const NEVER_FLAGGABLE: readonly string[];
|
|
|
80
85
|
* Every server-side gate resolved through PostHog.
|
|
81
86
|
*
|
|
82
87
|
* Intentionally empty: introducing the mechanism is not a reason to move
|
|
83
|
-
* existing switches onto it. The
|
|
88
|
+
* existing switches onto it. The env gates above stay in Doppler, and a
|
|
84
89
|
* new entry belongs here only when its worst wrong value is a cosmetic or
|
|
85
90
|
* reversible product difference — never spend, external writes, or telemetry
|
|
86
91
|
* egress.
|
|
@@ -62,6 +62,11 @@
|
|
|
62
62
|
* - OXYGEN_AGENTS_ENABLED — (b). Authorizes autonomous Agent run execution —
|
|
63
63
|
* tool authority AND credit spend (`apps/web/src/lib/agent-runtime.ts`,
|
|
64
64
|
* `apps/worker/src/agent-cycle.ts`). Same class as publishing.
|
|
65
|
+
* - OXYGEN_REPRICING_2026_09_EFFECTIVE_AT — (b). The instant every held price
|
|
66
|
+
* rise and limit cut of the 2026-09 repricing takes effect
|
|
67
|
+
* (`packages/shared/src/repricing.ts`). A remotely flippable value would let
|
|
68
|
+
* a mis-click move every customer's prices; it changes only through Doppler,
|
|
69
|
+
* with explicit human approval.
|
|
65
70
|
*
|
|
66
71
|
* The list is a guard, not a to-do: none of these is migrated, and
|
|
67
72
|
* `findFeatureGateRegistryViolations` fails the build's tests if one ever is.
|
|
@@ -76,12 +81,13 @@ export const NEVER_FLAGGABLE = [
|
|
|
76
81
|
"OXYGEN_LLM_TRACING_ENABLED",
|
|
77
82
|
"OXYGEN_TRIGGER_DEFAULT_CAPS",
|
|
78
83
|
"OXYGEN_BYOK_DAILY_CAPS",
|
|
84
|
+
"OXYGEN_REPRICING_2026_09_EFFECTIVE_AT",
|
|
79
85
|
];
|
|
80
86
|
/**
|
|
81
87
|
* Every server-side gate resolved through PostHog.
|
|
82
88
|
*
|
|
83
89
|
* Intentionally empty: introducing the mechanism is not a reason to move
|
|
84
|
-
* existing switches onto it. The
|
|
90
|
+
* existing switches onto it. The env gates above stay in Doppler, and a
|
|
85
91
|
* new entry belongs here only when its worst wrong value is a cosmetic or
|
|
86
92
|
* reversible product difference — never spend, external writes, or telemetry
|
|
87
93
|
* egress.
|
|
@@ -10,6 +10,7 @@ export * from "./capability-discovery.js";
|
|
|
10
10
|
export * from "./user-capability-routing.js";
|
|
11
11
|
export * from "./plan-capabilities.js";
|
|
12
12
|
export * from "./plan-limits.js";
|
|
13
|
+
export * from "./plan-band.js";
|
|
13
14
|
export * from "./sending-seats.js";
|
|
14
15
|
export * from "./sending-seat-capacity.js";
|
|
15
16
|
export * from "./plain-support-events.js";
|
|
@@ -37,7 +38,7 @@ export * from "./credit-guidance.js";
|
|
|
37
38
|
export * from "./deploy-env.js";
|
|
38
39
|
export * from "./directory.js";
|
|
39
40
|
export * from "./email-dsn.js";
|
|
40
|
-
export * from "./email-
|
|
41
|
+
export * from "./email-hard-bounce.js";
|
|
41
42
|
export * from "./email-tracking-token.js";
|
|
42
43
|
export * from "./email-unsubscribe-token.js";
|
|
43
44
|
export * from "./error-redaction.js";
|
|
@@ -10,6 +10,7 @@ export * from "./capability-discovery.js";
|
|
|
10
10
|
export * from "./user-capability-routing.js";
|
|
11
11
|
export * from "./plan-capabilities.js";
|
|
12
12
|
export * from "./plan-limits.js";
|
|
13
|
+
export * from "./plan-band.js";
|
|
13
14
|
export * from "./sending-seats.js";
|
|
14
15
|
export * from "./sending-seat-capacity.js";
|
|
15
16
|
export * from "./plain-support-events.js";
|
|
@@ -37,7 +38,7 @@ export * from "./credit-guidance.js";
|
|
|
37
38
|
export * from "./deploy-env.js";
|
|
38
39
|
export * from "./directory.js";
|
|
39
40
|
export * from "./email-dsn.js";
|
|
40
|
-
export * from "./email-
|
|
41
|
+
export * from "./email-hard-bounce.js";
|
|
41
42
|
export * from "./email-tracking-token.js";
|
|
42
43
|
export * from "./email-unsubscribe-token.js";
|
|
43
44
|
export * from "./error-redaction.js";
|