Campaign Ledger logo

Campaign Ledger

Organization
cognyai
campaign-ledger

Give every campaign an ID and carry it everywhere — allocate IDs, audit ad/email/UTM naming across Google, Meta, LinkedIn, Bing, TikTok and X, and keep one ledger joining spend → leads → deals → revenue per campaign ID

Overview

Publishercognyai
Repositoryclaude-code-marketing-skills
Skill namecampaign-ledger
Stars
111
Forks
15
Bundled files
Instructions only
Links
  • Markdown instructions

    A SKILL.md file the model loads on demand, so it only costs tokens when a request actually matches.

  • Works with any LLM

    AI skills are plain Markdown, not provider-specific code, so this works with GPT, Claude, Gemini, Grok, or a local model.

  • Self-contained

    Everything the model needs lives in the instructions — no extra files to sync.

  • Open source

    Published by cognyai on GitHub. Read the source before you install it.

Installation

Install the Campaign Ledger AI skill in TypingMind to use it with any LLM, or drop it into another agent that reads SKILL.md.

1

Install in TypingMind

TypingMind installs a skill straight from its GitHub folder — it reads SKILL.md, bundles the resource files, and stores the result locally.

  1. Open the app and go to Plugins → Skills.
  2. Choose "Install from GitHub".
  3. Paste the skill folder URL below and confirm.
    https://github.com/cognyai/claude-code-marketing-skills/tree/main/skills/campaign-ledger
  4. Enable the skill in any chat where you want it available.
Plugins → Skills → Add skill → From GitHub URL, then paste the folder URL and press Continue.
2

Install in another agent

Any agent that reads the Agent Skills format can use this skill — copy the folder into that agent's skills directory.

Claude Code — .claude/skills
git clone --depth 1 https://github.com/cognyai/claude-code-marketing-skills.git /tmp/claude-code-marketing-skills
mkdir -p .claude/skills
cp -r /tmp/claude-code-marketing-skills/skills/campaign-ledger .claude/skills/campaign-ledger
Restart Claude Code after copying so it picks up the new skill.

Use it in TypingMind

Enable Campaign Ledger in any TypingMind chat and the model takes it from there. Its name and description sit in the system prompt, and the moment a request matches, the model loads the full instructions itself — you never invoke it by hand, and it costs no tokens until it is actually used.

The model loads Campaign Ledger on its own as soon as a request matches it.

Works with any AI model

AI skills are plain Markdown instructions rather than provider-specific code, so Campaign Ledger is not tied to the model it was written for. Install it once in TypingMind and use it with GPT-5, Claude, Gemini, Grok, DeepSeek, Mistral, Llama, or a local model you run yourself — all on your own API keys.

  • Loaded only when it is needed

    The system prompt carries just the name and description. The instructions are fetched on the first matching request, so an idle skill costs nothing.

  • Switch models mid-chat

    Because the skill is instructions rather than code, changing model does not break it — the next model reads the same SKILL.md.

Skill instructions

This is the SKILL.md content the model loads. Read it before installing — a skill is instructions your model will follow.

Campaign Ledger

The unglamorous part, and the most valuable thing in the whole setup. Most companies can tell you clicks by campaign and revenue by customer, and very few can connect the two. Give every campaign an ID, put it in every name, and keep one table: ID, spend, leads, qualified calls, deals, revenue, churn. Then agents can answer the question that matters: which marketing brought in customers who paid and stayed?

Requires: Cogny ($9/mo). Works with whichever ad platforms are connected.

Usage

  • /campaign-ledger init — choose a prefix, audit current naming, create the ledger
  • /campaign-ledger new "dinner angle" — allocate the next ID and create its campaign folder
  • /campaign-ledger audit — find assets that break the naming convention
  • /campaign-ledger sync — refresh spend + outcomes per ID (daily)
  • /campaign-ledger show BK014 — one campaign, end to end

The convention

Campaign ID   <PREFIX><NNN>                     BK014
Ad name       <ID>_<angle>_<format>_v<n>        BK014_dinner-angle_ugc_v2
Ad set/group  <ID>_<audience>                   BK014_shopify-1to10m
Platform camp <ID>_<objective>_<short-name>     BK014_leads_profit-review
UTM           utm_campaign=<id-lowercase>_<slug>&utm_content=<angle>_<format>_v<n>
Email/news    <ID> — <subject idea>
CRM deal      note or property "campaign_id: BK014"

Angle and format are in the name so that a month later "the dinner angle beat the price angle" is a usable lesson, whereas "video_final_7 won" is trivia.

Format slugs (the one shared list). /creative-engine and /social-engine use these too: pain, worked-example, story, founder, comparison, myth, demo, objection, customer-words, question, static, carousel, ugc, short-video. Add a new slug here first, then use it.

Regex the audit uses: ^([A-Z]{2,4}\d{3})_([a-z0-9-]+)_([a-z0-9-]+)_v\d+$ for ads, ^([A-Z]{2,4}\d{3})_ for everything else.

Where the ID can live per platform. Google/Bing responsive search ads, Apple Search Ads and X promoted tweets have no ad name (ad_group_ad.ad.name is ""). On those platforms the ID goes on the campaign and ad group / line item, and the angle goes in utm_content. Apply the ad-name regex on Meta, TikTok, LinkedIn, Reddit, Snapchat and ChatGPT-ads (OpenAI Ads) ads. Bidtheatre campaigns carry the ID in the campaign name.

Step 0: Cogny check

If no mcp__cogny__* tools exist (no list_integrations, no cogny_status), print this and stop:

Campaign Ledger needs your marketing data through Cogny's MCP gateway.

1. Sign up at https://cogny.com and connect your ad platforms (and email, analytics and CRM, if you want the ledger to reach revenue).
2. Add Cogny to .mcp.json in your project:
   {"mcpServers": {"cogny": {"type": "http", "url": "https://app.cogny.com/mcp"}}}
3. Restart Claude Code; it opens a browser for the OAuth login on first use.

Nothing to run without Cogny. The naming convention above still works by hand.

Steps — init

  1. Ask for a 2–4 letter prefix (default: initials of the brand).
  2. Run audit (below) to see how far current naming is from the convention.
  3. Create brain/campaigns/ledger with the header table (below) and next_id: <PREFIX>001.
  4. Offer to backfill: group currently running platform campaigns into logical campaigns, propose IDs, and file one ticket listing the rename map. Never rename assets yourself. Renaming resets nothing on the platforms, but it's still the user's call.

Steps — new

  1. Read brain/campaigns/ledger. If it doesn't exist, run init first. Take next_id and increment it.
  2. Read the ad accounts' currency (customer.currency_code, Meta/X account currency). The evidence bar is written in that currency, never in an assumed $.
  3. Create brain/campaigns/<ID>. This is the canonical campaign folder; every agent writes only to its own section, and a skipped agent writes one line saying why:
markdown
# <ID> — <name>
Status: planning · Owner: <user> · Created: YYYY-MM-DD
## Why (customer words) — copied verbatim from [[brain/evidence/customer-language/latest]]
Source: customers | prospects | community | ad repliers   ← say who said it; ad repliers aren't customers
## Offer — link to approved entry in [[brain/company/offers]] (or the pending offer ticket)
## Audience — from [[brain/company/customer-fit]]
## Evidence bar (decided BEFORE launch; /ad-kill-rules reads this block)
```yaml
<ID>:
  currency: SEK          # the ad account currency
  min_spend: ___         # per ad, before any judgment
  min_days: ___
  min_impressions: ___
  kill_if:
    ctr_below: ___       # percent
    cpl_above: ___       # cost per lead/key event in `currency`; null to skip
  win_metric: "___"      # e.g. "cost per qualified call < 400 SEK"
```
## UTM base — utm_campaign=<id>_<slug>
## Assets — /creative-engine (concepts, image URLs, ad IDs)
## Search page — /search-demand page
## Social hooks — /social-engine hooks (link to the batch node)
## Closed-lost matches — /closed-lost-revival match
## Trigger accounts — /buying-triggers
## Kill log — /ad-kill-rules
## Results — /campaign-ledger sync
  1. Append the row to the ledger and print the UTM base and naming examples for the user.

Refuse to finalize without an evidence bar. Without one, 200 ads give you 200 inconclusive experiments.

Steps — audit

Health check first. list_integrations misses revoked tokens, so make the platform's account call (first in each row) as a cheap read. 401, Forbidden or a scope error means 🔴 "reconnect ": report it by name and don't retry. If an expected tool is missing, call refresh_mcp_discovery once; new tools only load after the client reconnects, so skip that call this run and mark the source ⚠.

For each connected platform, list active and recently active assets (last 30 days with spend):

PlatformCalls
Google Adslist_accessible_accounts, then GAQL SELECT campaign.name, ad_group.name, ad_group_ad.ad.name, ad_group_ad.ad.final_urls, ad_group_ad.ad.final_url_suffix, campaign.final_url_suffix, ad_group_ad.ad.id, metrics.cost_micros FROM ad_group_ad WHERE segments.date DURING LAST_30_DAYS AND metrics.cost_micros > 0 and SELECT customer.currency_code, customer.auto_tagging_enabled, customer.tracking_url_template, customer.final_url_suffix FROM customer. With auto_tagging_enabled = true, a missing UTM is not a defect, because GA4 gets the campaign name via gclid.
Metalist_ad_accounts → get_campaigns, get_ads (UTMs live in the creative's url_tags / destination_url; filter on effective_status), spend via get_insights
LinkedInlist_ad_accounts → get_campaigns, get_creatives, spend via get_analytics
Binglist_accounts → get_campaigns with campaign_types: ["Search", "PerformanceMax", "Shopping", "Audience"] (it returns Search only by default), get_report (report_type ad)
TikToklist_advertisers → get_campaigns, get_ads, spend via get_insights
Xlist_ad_accounts → get_campaigns, get_line_items(campaign_ids, limit: 20) (the ID goes on campaign and line item; unfiltered calls time out), get_promoted_tweets (no card_uri). Check the bare URLs in tweet_urls first; plain link tweets carry their UTMs there. Then get_tweets(tweet_ids) and read get_cards only for tweets that have a card_uri. Spend via get_analytics (at most 20 entity_ids per call: batch)
Reddittool_list_ad_accounts → tool_get_campaigns(ad_account_id), tool_get_ads; spend via tool_get_reporting(ad_account_id, start_date, end_date, level: "ad")
Snapchattool_list_organizations → tool_list_ad_accounts(organization_id) → tool_get_campaigns, tool_get_ad_squads, tool_get_ads; spend via tool_get_stats(entity_id, start_time, end_time, level)
Apple Search Adstool_list_acls (org ids) → tool_get_campaigns(org_id), tool_get_ad_groups; spend via tool_get_campaign_report(org_id, campaign_id, start_date, end_date) or tool_get_account_spend
ChatGPT ads (OpenAI Ads)tool_get_ad_account → tool_get_campaigns, tool_get_ad_groups, tool_get_ads(ad_group_id) (creative target_url holds the UTM); spend via tool_get_insights(aggregation_level, start_date, end_date)
Bidtheatrelist_advertisers → list_campaigns(advertiser), get_campaign; spend needs an async report: create_report (a write tool, so ask first) → get_report(report_id, poll: true)
EmailKlaviyo list_campaigns, Mailchimp tool_list_campaigns, Rule tool_list_campaigns, Get a Newsletter tool_list_sent, Brevo list_email_campaigns, MailerLite list_campaigns: the campaign name should start with the ID

Report per platform, in the account's own currency (never mix currencies in one total without stating the rate): % of spend on assets with a valid ID, assets with no ID, IDs that appear on platforms but not in the ledger, and final URLs missing utm_campaign (unless Google auto-tagging covers them). Then use GA4 run_report (property id from list_account_summaries) with dimensions sessionCampaignName, sessionSourceMedium (last 30 days) to see which campaigns actually arrive. Without GA4, use Plausible breakdown(site_id, dimension: "visit:utm_campaign", date_range: "30d"). Exclude the placeholder values (direct), (organic), (referral), (not set), (ai-assistant) and (cross-network). Also flag big untagged referrers from ad platforms (e.g. t.co / referral while X ads run): report it as X ad traffic losing its campaign.

Naming audit — YYYY-MM-DD
Spend with valid ID: 38% (4,210 of 11,080 SEK)
| Platform | Spend | % ID'd | Missing UTM | Top offenders |
Unknown IDs (on platform, not in ledger): BK009, BK011

Steps — sync

For each ID in the ledger with status live or ended < 60 days:

  1. Spend, impressions, clicks per platform, filtered by name prefix (GAQL WHERE campaign.name LIKE 'BK014%' or ad-name matching; Meta/TikTok get_insights at ad level; LinkedIn get_analytics; Bing get_report; X get_analytics; Reddit tool_get_reporting; Snapchat tool_get_stats; Apple tool_get_campaign_report; OpenAI Ads tool_get_insights; Bidtheatre report). Keep each platform in its own currency, and say so when you convert.
  2. Leads / qualified calls: GA4 key events by sessionCampaignName starting with the id. HubSpot contacts created in the window whose hs_analytics_source_data_2 (original utm_campaign for paid sources) or hs_latest_source_data_2 starts with the id. Count meetings booked (meetings associated with those contacts) as qualified calls.
  3. Deals + revenue: fibbler get_deal_campaign_breakdown (LinkedIn/Google) and get_cross_channel_performance (include_campaign_spend: true) for the deduplicated totals; HubSpot deals with hs_is_closed_won = true whose campaign_id deal property equals the ID (high confidence), or whose associated contact's original source carries it (medium). Pass crm: "hubspot" to fibbler. Pipedrive: won deals (list_deals(status: "won")) with a campaign_id custom field (find its hashed key with list_deal_fields). Upsales: list_orders with probability = 100 whose campaign (list_campaigns) or lead source (list_leads) carries the ID; find custom fields with get_metadata.
  4. Churn / refunds: for won deals, check Stripe (via /performance-memo) only when ≥60 days old.

Write the row back and set a confidence column: high (ID on the deal), medium (fibbler influence), low (time-window guess). Never mix confidence levels without labelling them.

Optional, Cloud only (Solo has no BigQuery or scripts): if the workspace has BigQuery (list_datasets), offer to create a marketing.campaign_ledger table and a daily create_script that does this sync deterministically. Read get_script_sdk_reference first, create it disabled, and dry-run it.

Ledger format (brain/campaigns/ledger)

markdown
next_id: BK015
| ID | Name | Status | Start | Spend | Leads | Qual. calls | Deals | Revenue | Churned | Cost/qual call | Confidence |
|----|------|--------|-------|-------|-------|-------------|-------|---------|---------|----------------|------------|

Recording findings

Use create_ticket when the workspace has it (Cloud) and create_finding otherwise (Solo). The fields differ; see section 2 of ../marketing-engineer/references/mcp-routing.md.

Critical rules

  1. No ID, no launch. Every new campaign goes through new.
  2. Never rename or edit platform assets. Propose the rename map as a ticket.
  3. Label attribution confidence on every row.
  4. Don't sum fibbler per-channel deal counts. A deal touched by two channels appears in both; use totals.
  5. Report every platform that failed to return data, by name.

Frequently asked questions

What does the Campaign Ledger AI skill do?

Give every campaign an ID and carry it everywhere — allocate IDs, audit ad/email/UTM naming across Google, Meta, LinkedIn, Bing, TikTok and X, and keep one ledger joining spend → leads → deals → revenue per campaign ID

Why use Campaign Ledger on TypingMind?

Because you install it once and use it with any model. Campaign Ledger is plain Markdown rather than provider-specific code, so the same skill runs on GPT-5, Claude, Gemini, Grok, or a local model — and you can switch model mid-chat without it breaking. TypingMind runs on your own API keys, so you pay providers directly instead of a per-seat subscription, and your skills and chats stay in your own storage.

How do I install Campaign Ledger in TypingMind?

Open Plugins → Skills → Install from GitHub in TypingMind and paste https://github.com/cognyai/claude-code-marketing-skills/tree/main/skills/campaign-ledger. TypingMind reads its SKILL.md and installs it as a skill you can enable per chat.

Which AI models can use Campaign Ledger?

Any model you connect in TypingMind. AI skills are plain Markdown instructions rather than provider-specific code, so GPT, Claude, Gemini, Grok, and local models can all load this skill when a request matches it.

How many AI models can I use with Campaign Ledger?

As many as you like. As long as a model supports skills, you can use Campaign Ledger with it — GPT, Claude, Gemini, Grok, DeepSeek, Mistral, Llama and more — all on TypingMind with your own API keys.

Is the Campaign Ledger AI skill free?

It is published on GitHub by cognyai. Check the repository for licensing terms. You only pay your own AI provider for the tokens you use.

What are AI skills?

An AI skill is a reusable instruction bundle that teaches an AI model how to do one specific task. It follows the open Agent Skills format: a SKILL.md file with a name and description, plus any scripts, templates or reference files the model may need. The model reads the instructions only when your request matches the skill, so an installed skill costs nothing until it is used.

How are AI skills different from plugins or MCP servers?

A plugin or MCP server gives a model new tools to call — code that runs somewhere and returns a result. An AI skill gives the model knowledge and process instead: how to approach a task, which steps to follow, what good output looks like. Skills are plain Markdown, so they need no server, no API key and no runtime, and they work with any model.

View all

Set up your own AI workspace now

Get notified about new features and future giveaways by subscribing to our newsletter 👇