# Marketing source inventory

A worksheet for bringing marketing data into one place. Fill one row per source **before** you migrate or rebuild anything. Start from the systems where events happen, not from the tables your apps read today.

Companion to part 2 of "From marketing apps to a dependable data foundation" on keboola.com.

## 1. The inventory

| Source | Event or record | Grain | Stable key | How it lands raw | Freshness rule | Fields that must survive | Known bad values | Owner |
|---|---|---|---|---|---|---|---|---|
| *Example: website visitor identification* | *Company visit* | *Account (person rarely)* | *Upstream visit id* | *Webhook stream* | *Newest visit under 24 h on weekdays* | *Page URL, referrer, company domain, timestamp, campaign tags* | *Placeholder emails, empty string, the text "null"* | |
| CRM | Contact, account, activity, opportunity | Person / account / opportunity | | | | | | |
| Forms and gated content | Submission | Person | | | | Campaign tags, asset, country, company, title | | |
| Email platform | Delivery, open, click, unsubscribe | Person / event | | | | Campaign, message, consent | | |
| Meeting booking | Booking, cancellation | Person / meeting | | | | | | |
| Product or demo analytics | Session, page view, demo step | Anonymous / account / person | | | | Campaign tags | | |
| Visitor identification | Company visit | Account | | | | | | |
| Outreach | Message sent, reply | Person / event | | | | | | |
| Paid media | Campaign, ad set, creative, spend, clicks | Campaign / day | | | | Creative name, landing page | | |
| Field events | Registration, attendance | Person / event | | | | | | |

**Grain** answers "one row is one what?". Decide it per source. A visit that names only a company is an account-level event, not a person.

## 2. Three checks per source

Run these on each raw table, and again after each transformation:

1. **Rows against distinct keys.** `SELECT COUNT(*), COUNT(DISTINCT <key>) FROM <table>`. If they differ on an incrementally loaded source, you are counting copies.
2. **Empty identities.** The share of rows whose identity field is NULL, an empty string, a placeholder address or the text `null`. Convert all of them to missing before any join: `NULLIF(NULLIF(TRIM(email), ''), 'null')`.
3. **Fields that survive.** For each field in the "must survive" column, compare its fill rate in the raw payload with its fill rate in your reporting table. A field empty downstream but present in the payload was dropped by you, not by the source.

## 3. Questions to answer before you call a source "connected"

- What did the source actually send? Keep every field in the raw layer, not only the ones you use today.
- Is this table a source or the output of other processes? Count what writes to it.
- What proves freshness for each event type you depend on, not only for the table?
- Is the record anonymous, account-level or person-level, and how confident is the link?
- How are repeat deliveries of the same event detected?
- Which business rule (for example "qualified lead") is applied later, and where is it defined once?
- Who responds when the output is incomplete or stale?
