Marketing skills — build targeting lists & processes from fcr-dashboard
What this is. FCR provides marketing with a process and a binding so they can build their own targeting lists and processes as Claude Code skills, from this repo. We do not dictate what they build or how they model their data — marketing own their own BigQuery dataset and schema, their Airtable bases, and whatever workflows they want, called via n8n.
We deliberately did not take the separate service-bound-worker path in
extending-with-workers.md. There is nofcr-marketing-apiworker. Skills read the data lake directly and route every write through n8n.
The split: what FCR provides vs what marketing owns
| FCR provides (the foundation) | Marketing owns (their call) | |
|---|---|---|
| Read | Read access to the data lake via fcr-dashboard-api (x-api-key) |
Which data they pull, how they filter a list |
| Write contract | A uniform skill→n8n envelope + a webhook registry | Which actions they add, what payloads they send |
| Shared-system writes | Governed HubSpot actions (create deal / note / static list) — FCR manages the scopes | When and what they push to HubSpot |
| Their-system writes | Nothing — it's theirs | Their BigQuery dataset + schema, their Airtable, ESPs, etc., wired in their own n8n |
| n8n runtime | The FCR cloud tenant hosts the shared HubSpot actions | Their own self-hosted n8n Community for their own flows |
| Guardrails | Writes go through n8n; shared/production data is read-only | Keep their writes in their own dataset/assets |
The one rule that protects everyone: writes go through n8n, and shared/production
systems are read-only to experiments. fcr_operations, SUPERSET_MASTER, etc. are
what the advisor, HubSpot card, and dashboard read live — never write them. Marketing's
own dataset and Airtable are theirs to shape however they like.
Content-admin curation (the one sanctioned non-n8n write path)
There is one deliberate exception to "writes go through n8n": the three
content-admin skills that let marketing own the knowledge the advisor reads.
These write through governed worker endpoints (x-api-key), not n8n — the same
pattern as am-email-templates and case-studies-reindex. This is content
curation, not targeting-list data, and each endpoint owns its own validated
write + soft-delete + audit, so the same guardrails (confirm-first, --dry-run,
reversible delete, audit trail) hold without n8n plumbing.
| Skill | Endpoint | What it curates | Where it lands |
|---|---|---|---|
/marketing-knowledge |
dashboard-marketing-knowledge |
Company-knowledge docs (positioning, FAQs, proof points) | shared fcr-company-knowledge index (content_type=marketing_doc) → advisor search_company_knowledge + /company-knowledge |
/marketing-products |
dashboard-marketing-products |
The FCR price list (price-of-record) + pricing rules | fcr_operations.marketing_products → re-renders the advisor's PRODUCT CATALOGUE block to KV (fcr-products.js is now only the fallback) + indexes each product into fcr-company-knowledge |
/marketing-outreach |
dashboard-marketing-outreach |
Marketing-approved outreach email copy | fcr_operations.marketing_outreach_templates → fcr-outreach-templates index → advisor find_outreach_template |
Every write is confirmed first, supports --dry-run, soft-deletes (re-adding
the same id restores it), and writes a row to fcr_operations.marketing_kb_audit.
De-index = soft-delete the row and deleteByIds the vectors.
The boundary still holds for everything else: targeting-list data writes
(deals, notes, static lists, marketing's own BQ dataset) still go n8n-only via
the registry — a skill must never call /dashboard-bq-admin, api.hubapi.com, or
Airtable directly. The content-admin endpoints above are the only worker write
surface a marketing skill may call, and only for the three corpora named here.
The binding
SKILL.md (Claude Code, in this repo)
├─ READ → fcr-dashboard-api (x-api-key) reads only, direct
└─ WRITE → n8n webhook (x-api-key) resolved via the action registry
├─ FCR cloud n8n: shared, governed actions (HubSpot deals/notes/lists)
└─ marketing's community n8n: their own actions
↓
their BigQuery dataset (their schema) · their Airtable · HubSpot (shared)
A skill never writes a database or calls HubSpot/Airtable directly — it POSTs to an n8n webhook that owns the write. That keeps powerful credentials in n8n, and means a buggy experiment can't corrupt the data reps read live.
The write contract
Every write is a POST with this envelope (header x-api-key):
{
"action": "<action name from the registry>",
"idempotency_key": "<stable hash of the inputs>",
"dry_run": false,
"source": { "skill": "<skill-name>", "actor": "<email>", "run_id": "<uuid>" },
"payload": { "...action-specific..." }
}
Response (synchronous — returned on the same call):
{ "ok": true, "action": "...", "deduped": false,
"result": { "ids": ["..."], "hubspot_url": "...", "count": 1 } }
or { "ok": false, "action": "...", "error": "...", "status": 4xx }.
idempotency_key— recommended on every write. Compute it deterministically from the inputs (e.g.sha1("create-deal|" + business_key + "|" + list_name)). If the workflow records keys (the recommended pattern — see below), a re-run returns the prior result withdeduped:trueinstead of writing twice. Re-running a list is then safe.dry_run:true— the workflow echoes what it would do and performs no side effect. Always offer a dry run before the first live write.
The action registry (FCR vs your community n8n)
Skills don't hard-code webhook URLs — they read
.claude/skills/_marketing/n8n-registry.json:
{
"tenants": {
"fcr": { "webhook_base": "https://fcrmedia.app.n8n.cloud/webhook", "key_env": "VITE_N8N_API_KEY" },
"community": { "webhook_base": "<your-box>/webhook", "key_env": "MARKETING_N8N_KEY" }
},
"actions": {
"create-deal": { "tenant": "fcr", "path": "marketing-create-deal" }, // shared, governed
"note-on-deal": { "tenant": "fcr", "path": "marketing-note-on-deal" }, // shared, governed
"build-static-list": { "tenant": "fcr", "path": "marketing-build-static-list" }, // shared, governed
"bq-append": { "tenant": "fcr", "path": "marketing-bq-append" } // writes to YOUR dataset
// add your own, e.g.:
// "airtable-sync": { "tenant": "community", "path": "marketing-airtable-sync" }
}
}
The skill resolves action → tenant → webhook_base, reads the tenant's key_env line
from .env.local, and POSTs. x-api-key is the header on every call.
Shared, governed actions (FCR-provided)
HubSpot is the company CRM, so writes to it run through FCR-owned workflows on the FCR cloud tenant, with HubSpot scopes managed centrally:
create-deal— create one deal per target (needscrm.objects.deals.write).note-on-deal— note on an existing deal (needscrm.objects.contacts.write+deals.write).build-static-list— create a MANUAL list + add members (needscrm.lists.write).
If one of these returns 403, the n8n HubSpot credential is missing a scope and needs re-authorizing — flag it to the dashboard owner.
Your own actions (marketing-owned)
Anything that touches your systems is yours to build, typically on your community n8n box:
- BigQuery → your dataset. You create and own the dataset and schema. A starter
bq-appendaction streams rows into a table you name; the workflow allowlists your dataset so it can never touchfcr_operations. The columns are whatever you defined. - Airtable. You have Airtable bases — wire an n8n workflow (Airtable node) and add
an action (e.g.
airtable-sync) to the registry under"tenant": "community". - Anything else (ESP, Slack, sheets) — same pattern: build the workflow, register the action, call it from a skill with the envelope.
Register a workflow on your own community n8n
- Build the workflow on your self-hosted n8n Community box: Webhook (POST) node + header-auth credential + whatever it does (BigQuery / Airtable / …).
- In the registry, set
tenants.community.webhook_baseto your box's/webhookbase, and add an action with"tenant": "community"and the webhookpath. - Add the box's key to
.env.localasMARKETING_N8N_KEY. Never reuse the FCR key (VITE_N8N_API_KEY) for the community box. - Secure the box — it holds real credentials. Put it behind Cloudflare Access / HTTPS,
never a bare public port (see
extending-with-workers.md§3).
Your BigQuery — you create it, you own the schema
Marketing create and own their dataset (e.g. marketing_ops) once, and define whatever
tables/schema they need — FCR doesn't dictate the data model. Two habits worth keeping:
- Write only to your dataset. Give the n8n BigQuery credential
dataEditoron your dataset (and read-only elsewhere), so an accident can't reach production tables. - Make state tables idempotency-friendly. For anything you don't want processed
twice (sends, deal creation), keep a unique key +
created_at/statusso a re-run dedupes. This is what makes theidempotency_keyabove actually safe.
Data rules to respect
Marketing skills inherit the rules in ../CLAUDE.md (see Data Rules
and Marketing Write Rules). The ones that bite list selection most:
- Ads spend = retail (what the client pays FCR). Never "media spend".
- KEYWORD_INTELLIGENCE / CATEGORY_BENCHMARKS are cumulative, not monthly.
- Golden Pages subscriber counts ≠ FCR clients — use them for competitive density.
- Citation URLs: every created HubSpot deal/list you report must be a clickable link, never a bare ID.
Checklist — adding a new marketing skill
- Clone the template: copy
.claude/skills/marketing-list/to.claude/skills/<your-skill>/and mirror to.agents/skills/<your-skill>/(keep both identical). - Set frontmatter:
name,description,argument-hint,user-invocable: true. - Write the source query — reads only, via dashboard API endpoints.
- Pick the writes — shared HubSpot actions and/or your own (BQ/Airtable) actions.
- Keep the confirm step +
--dry-run— never write without showing the user first. - Add
idempotency_keyto every write. - Test each path end-to-end: dry-run → live → re-run (expect
deduped). - For HubSpot actions, confirm scopes are granted on the n8n credential (else 403).
Compliance
If a process ends in outreach email, it needs consent + unsubscribe (GDPR) and SPF/DKIM on the sending domain — settle before the first send.
Related: extending-with-workers.md (broader team-environment
pattern; the separate-worker path is not used here), n8n-workflows.md,
the template skill .claude/skills/marketing-list/SKILL.md,
the registry .claude/skills/_marketing/n8n-registry.json.
FCR Dashboard documentation · generated from docs/ · keep counts verified, not guessed.