← Hub
Guía de Ingesta · Todos los canales Shakers Reporting

Guía de Ingesta por Canal

Referencia completa: método de ingesta, credenciales requeridas, proceso mensual y contrato de datos (schema) para cada canal del hub.

Resumen rápido
Canal North star Fuente Script Método
Google AdsCPSQLSupermetrics AW · 3453900141pull-google-ads.pyAuto
LinkedIn AdsCPSQLSupermetrics LIA · 509238676pull-linkedin-ads.pyAuto
SEO / OrgánicoClicksGSC API + GAWA GA4 PROpull-seo.pySemi
LI OrgánicoEng. rateSupermetrics LIPpull-social.pyAuto
InstagramReachSupermetrics IGIpull-social.pyAuto
YouTubeAvg view %Supermetrics YT2pull-social.pyAuto
Web Analyticsgenerate_lead (solo SQL)GA4 Data API · SA calero-marketing-analytics + HubSpotpull-web-analytics.pyAuto
EventosAsistentesHubSpot marketing/v3 + forecast costespull-events.pySemi
OutboundAprobadasTheirStack + IA classifier (4 CSVs)build-outbound.pySemi
BrandBranded clicksGSC (SA clawdbot) + GA4 Direct (SA mkt)pull-brand.pyAuto
AEOCitation SoVTool propia (export .md) · legacy: Bing WMT + HubSpot AEO betaimport-aeo-tool.pyManual
InflushakersEmpleados posteandoxlsx equipo Social Sellingimport-social-selling.pyManual
PRClippings4 xlsx de agencia (PT·ES·IT·FR)import-pr.pyManual
💼
LinkedIn Ads
North star: CPSQL · Cuenta LIA 509238676
Automático
Conector
Supermetrics — LinkedIn Ads (LIA)
account: 509238676
Script de pull
deploy/pull-linkedin-ads.py --month YYYY-MM
Output
data/linkedin-ads/monthly/YYYY-MM.json
Pipeline HubSpot
Lee deals en HubSpot. closedwon = Rechazado (no ganado). Ganado = dealstage Active (1221750) o posterior.
Credenciales requeridas
VariableDescripciónObligatoriedad
SUPERMETRICS_API_KEYAPI key de SupermetricsOBLIGATORIO
Proceso mensual
  1. Incluido en build-and-deploy.sh. Se ejecuta automáticamente.
  2. El script extrae spend, impresiones, clics, CTR y conversiones SQL/MQL de la cuenta LIA.
  3. El split Supply/Brand se determina client-side en el HTML por tokens en el nombre de campaña (SUPPLY, TALENT, CG-PEOPLE).
  4. Sin validador dedicado. Si los valores son nulos el HTML muestra .
Filtros de string en Supermetrics LIA están rotos. Devuelven vacío sin error. Todo el split Brand/Supply es client-side. Nunca filtrar por nombre de campaña en el query de Supermetrics.

deal amount = GMV, no revenue. Revenue ≈ 20% del GMV.
Supply: registro no es formulario (desde agosto 2026)

LinkedIn tiene dos conversiones distintas en las campañas de captación de talento y hasta el 2026-08-03 el script las sumaba en un solo campo (talent_registrations). Esa métrica inflaba el volumen y hundía el coste por registro. Ahora se separan con la misma semántica que lead_status_supply en HubSpot y que el report de Google Ads:

BucketRegla sobre el nombre de la conversiónQué es
registroscontiene REGISTRATION o REGISTROAlta real de talento en la plataforma. Equivale a lead_status_supply = User.
formscontiene MQL o FORMFormulario de interés sin registro detrás. Equivale a lead_status_supply = MQL.
otroscualquier otro nombreNo se descarta: se publica en conversions_by_name y sube una fila de salud, para que un nombre nuevo no desaparezca en silencio.
Ene-Jun 2026 están reexpresados (2026-08-03). Cada uno de esos meses lleva un bloque restatement con lo publicado y lo reexpresado, y una fila de salud. El demand no cambió en ningún mes. Junio pasó de 143 conversiones a €17,10 a 19 registros a €128,67 más 127 formularios a €19,25.

LinkedIn reatribuye meses cerrados. Repullear Feb y Abr hoy devuelve menos conversiones supply que el pull original del 2026-06-23 (169 vs 299 y 347 vs 524). Donde pasa, el mes lleva restatement.vintage_note y una fila de salud de Vintage. No repullear meses cerrados sin necesidad: borra los parches manuales.
Cross-check CRM (manual)

El bloque crm_crosscheck contrasta lo que declara LinkedIn contra lead_status_supply en HubSpot. Es manual y cada pull lo deja vacío. No está automatizado porque HubSpot no recibe la campaña de LinkedIn: hs_analytics_source_data_1 vale literalmente "LinkedIn" en todos los contactos de PAID_SOCIAL, así que una lente CRM por campaña no es posible hoy. En julio 2026 el contraste dio 320 registros en plataforma contra 64 en CRM (105 sumando SOCIAL_MEDIA), y 408 formularios contra 837. Discrepancia abierta.

Consulta de referencia: contactos con createdate en el mes, lead_status_supply presente y hs_analytics_source = PAID_SOCIAL, agrupados por estado. Control de sanidad: PAID_SEARCH debe cuadrar con los registros del report de Google Ads del mismo mes.
Message Ads no reportan impresiones

Las campañas *_MSG_v1 devuelven impressions = 0 y clicks = 0 con gasto positivo. En julio fueron €3.591,45. El campo supply.spend_sin_impresiones lo declara y sube una fila de salud, porque si no el CTR y la frecuencia se leen como si cubrieran todo el presupuesto.

Contrato de datos (schema v1.1)
meta { account, period, window, sql_conversion_action, supply_exclude_tokens, supply_buckets, demand_status, north_star, source, refreshed_at, schema_version } hero { spend, impressions, reach, frequency, clicks, ctr, leads, mql, mql_status, sql, cpl, cpsql } _notes [ { type, text } ] portada del Overview funnel [ { stage, spend, impressions, clicks, ctr, leads, sql } ] pipeline { status:"manual", influenced_eur, opportunities, companies_touched, cpo_influenced } crm_crosscheck { status:"manual", platform:{...}, crm:{...}, verdict, note } campaigns [ { name, objective, status_raw, dot, spend, impressions, reach, clicks, ctr, leads, mql, sql, cpsql, cpl } ] residual_conversions [ { campaign, conversion, value } ] convierten sin entrega en el mes supply { spend, impressions, clicks, lgf_leads, registros, forms, otros, cost_per_registro, cost_per_form, spend_sin_impresiones, conversions_by_name:[ { name, bucket, value } ], campaigns:[ { name, objective, status_raw, spend, impressions, clicks, lgf_leads, registros, forms, cost_per_registro, no_impressions } ] } prev_period { label, window, spend, sql, mql, leads, cpl, cpsql } prev_supply { label, spend, registros, forms, cost_per_registro, cost_per_form } restatement { date, reason, published, restated, vintage_note } solo Ene-Jun 2026 audiences { job_function, seniority, company_size, industry } formats [ { type, name, campaign, impressions, clicks, ctr, leads } ] health [ { scope, signal, value, status } ]
CPSQL no va en JSON. Se calcula en HTML como spend/sql. El coste por registro y por formulario sí van precalculados porque el denominador no es evidente.

meta.demand_status = "paused" cuando las campañas cliente no tuvieron entrega en el mes: el hero sale a cero legítimamente y sube una nota de portada. Julio 2026 es el primer mes así.
🔍
SEO / Orgánico
North star: Clicks · GSC + HubSpot (lente CRM)
Semi-automático
Conexiones
Google Search Console (OAuth / SA)
Supermetrics GAWA → GA4 PRO 304414381
Script de pull
deploy/pull-seo.py --month YYYY-MM
Setup inicial
Ver deploy/README-gsc-setup.md para configurar OAuth GSC
Output
data/seo/monthly/YYYY-MM.json
Manifest usa "id" (no "slug") — intencional
Credenciales requeridas
Variable / ArchivoDescripciónObligatoriedad
SUPERMETRICS_API_KEYPara pull GA4 vía GAWAOBLIGATORIO
GSC_CREDENTIALS_FILEPath al JSON OAuth de GSC (default: deploy/gsc-credentials.json)OBLIGATORIO
deploy/clawdbot-gsc-reader-sa.jsonSA file (gitignored, nunca committear)OBLIGATORIO
Proceso mensual
  1. Verificar que deploy/gsc-credentials.json existe (o variable GSC_CREDENTIALS_FILE apunta al SA file).
  2. Ejecutar bash deploy/build-and-deploy.sh. Si OAuth no está configurado, el panel muestra banner "Pendiente primer pull" sin romper el resto.
  3. El script combina datos de GSC (clicks, impresiones, posición, keywords) con GA4 (sesiones de origen orgánico).
  4. Split NB/Brand: keyword.includes('shakers') client-side en el HTML.
SA file (clawdbot-gsc-reader-sa.json) está gitignored. Nunca committear. Si se pierde, regenerar desde GCP Console y actualizar la variable de entorno.
Lente CRM (desde agosto 2026)

Hasta el 2026-08-03 el canal publicaba hero.sql = null con sql_status = PENDING_HUBSPOT. Ya no: pull_hubspot_organic() consulta HubSpot con la misma definición de SQL que los canales de pago (createdate en el mes, lifecyclestage IN salesqualifiedlead/opportunity/customer, contactos de test QA Shakers excluidos), cambiando solo la fuente de origen.

Fuente hs_analytics_sourceQué esDónde sale
ORGANIC_SEARCHBúsqueda orgánica clásicahero.sql, hero.registros_talento y pestaña Negocio · CRM
AI_REFERRALSTráfico derivado de asistentes de IAaeo.crm · no se suma al orgánico
Backfill Ene-Jun 2026 (2026-08-03), sin repullear GSC. Los seis meses publicados llevan meta.crm_backfilled_at y se verificó por assert que ninguna métrica de Search Console del hero cambiara. La serie es homogénea desde enero pero no se publicó mes a mes.

El SQL de HubSpot mezcla demand y talento en el mismo lifecyclestage (fallo 2 del diagnóstico de tracking, sin resolver), así que crm.sources.*.sql_de_talento declara cuántos son de talento en vez de esconderlo. Flag --skip-crm para saltarse HubSpot.
Alcance de la propiedad y subdominios

La propiedad es sc-domain:shakersworks.com, así que el hero incluye www + blog + know + careers. El bloque subdomains los desglosa.

Dos correcciones del 2026-08-03.

1. meta.know_excluded: true era engañoso: la exclusión de know.shakersworks.com aplica solo a la tabla de top páginas, no al hero, donde sí suma. Ahora son dos flags: know_excluded_del_hero: false y know_excluded_de_top_pages: true.

2. El cluster blog no tenía filtro de sección y se publicaba con 0 clics teniendo tráfico real (julio: 231 clics y 67.881 impresiones). Aparecía por el efecto colateral de clusters.setdefault(cluster, {}) al recorrer las keywords tracked. Ahora filtra por blog.shakersworks.com, que es justo lo que el mapa de etiquetas de la ficha ya asumía. Backfilleado en Ene-Jun de forma aditiva. Un cluster sin filtro se marca con no_path: true en vez de publicarse como cero.
No calcular porcentajes de reparto sumando subdominios. En Search Console las cifras a nivel de página y los totales de la propiedad se cuentan distinto: una SERP que muestra dos páginas del sitio cuenta una impresión por página y solo una en el total. En julio los clics por host suman 3.588 contra 3.528 del total, y en impresiones la desviación es mucho mayor.
Contrato de datos (schema v1.1)
meta { site, period, window, prev_window, north_star:"Clicks", source, know_excluded_del_hero, know_excluded_de_top_pages, hosts_incluidos, crm_backfilled_at, hosts_backfilled_at, refreshed_at, schema_version } hero { clicks, impressions, ctr, avg_position, nb_clicks, nb_impressions, brand_clicks, sql, sql_status, registros_talento } prev_period { label, window, clicks, nb_clicks, impressions, avg_position } crm { queried_at, lens, sql_definition, sources: { ORGANIC_SEARCH | AI_REFERRALS: { sql, sql_qa_excluidos, sql_de_talento, registros, forms } } } subdomains [ { host, pages, clicks, impressions, ctr, avg_position } ] brand_split [ { type:"Brand"|"Non-brand", clicks, impressions, ctr, avg_position } ] top_pages [ { url, clicks, impressions, ctr, avg_position } ] sin know clusters [ { id, path_prefix, no_path, clicks, impressions, ctr, avg_position, terms:[ { cluster, type, q, volume, cpc_eur, intent, position, position_prev, delta, clicks, impressions, ctr } ] } ] quick_wins [ … mismo shape que terms … ] pos 4-10, impr >50, CTR <2% device_split [ { device, clicks, impressions, impr_pct, ctr, avg_position } ] aeo { featured_snippets, rich_results, note, source, crm: { sql, registros, forms, note } } health [ { scope, signal, value, status } ]
La posición media del Non-brand en brand_split es parcial: se calcula sobre las 5.000 queries visibles del API, no sobre el total. El hero sí usa el agregado real de la propiedad.

El north star es Clicks y la segunda métrica Posición media, que son las dos que muestra la tarjeta del hub. Hasta agosto 2026 meta.north_star decía "NB clicks" aunque el hero de la ficha ya medía clics totales. El SQL orgánico es KPI de negocio, no north star.
👥
LinkedIn Orgánico · Instagram · YouTube
North stars: Eng. rate · Reach · Avg view % — JSON compartido en data/social/monthly/
Automático
Conexiones
Supermetrics LIP (LinkedIn Pages)
Supermetrics IGI (Instagram Insights)
Supermetrics YT2 (YouTube Analytics)
Script de pull
deploy/pull-social.py --month YYYY-MM
Un solo script, tres plataformas. JSON compartido.
Output
data/social/monthly/YYYY-MM.json
Leído por LI Orgánico, Instagram, YouTube y Social (legacy)
Canales que leen este JSON
channels/linkedin-organic/
channels/instagram/
channels/youtube/
channels/social/ (legacy)
Credenciales requeridas
VariableDescripciónObligatoriedad
SUPERMETRICS_API_KEYAcceso a LIP, IGI y YT2OBLIGATORIO
Proceso mensual
  1. Incluido en build-and-deploy.sh. Ejecutar el día 1 del mes siguiente.
  2. Pull de los tres conectores en secuencia. Si uno falla, los otros siguen y el JSON lleva el campo en null.
  3. new_followers de Instagram solo disponible para los últimos 30 días vía IGI. Si el pull es muy tardío, ese campo llega a null (esperado).
  4. avg_view_pct YouTube = métrica a nivel canal, no por vídeo individual.
Instagram new_followers: si el pull se ejecuta después del día 30 del mes siguiente, la API IGI devuelve error 500. Campo queda null. Planificar pull antes del día 28.
Contrato de datos Social v1.0
meta { schema, period, month, window, accounts, north_stars, source, refreshed_at, status } linkedin hero { impressions, clicks, likes, comments, shares, engagements, engagement_rate, new_followers, new_followers_org, total_followers, total_followers_org } posts [ { title, url, media_type, impressions, engagements, engagement_rate, clicks, likes, shares } ] health [ { scope, signal, value, status } ] instagram hero { reach, profile_views, interactions, total_followers, new_followers } posts [ { type, product_type, permalink, caption, reach, like_count, comments_count, saves, shares, interactions } ] reels { count, avg_watch_time_min, total_views, total_interactions, avg_skip_rate } health [ { scope, signal, value, status } ] youtube hero { views, minutes_watched, avg_view_pct, new_subscribers, lost_subscribers, net_subscribers, total_subscribers, likes, shares } videos [ { title, url, published, views, minutes_watched, avg_view_pct, likes, shares } ] traffic_sources [ { source, views, minutes_watched } ] health [ { scope, signal, value, status } ]
📈
Web Analytics
North star: generate_lead · GA4 PRO 304414381 · automatizado desde agosto 2026
Auto
Conexión
GA4 Data API directa (SA calero-marketing-analytics)
property: 304414381
+ HubSpot para el contraste de SQL
Script de pull
deploy/pull-web-analytics.py --month YYYY-MM
Creado el 2026-08-03. Ya no hay pull manual.
Scope de datos
www.shakersworks.com
blog.shakersworks.com
know.shakersworks.com
Excluye: app y academy
Output
data/web-analytics/monthly/YYYY-MM.json
Credenciales requeridas
VariableDescripciónObligatoriedad
GA4_SA_FILESA con Viewer en la property PRO (default ~/.config/gcloud/shakers-mkt-sa.json)OBLIGATORIO
HUBSPOT_PRIVATE_APP_TOKENContraste de SQL de demand. Sin él, crm_contrast queda a nullRecomendado
Automatización (2026-08-03)

El canal se rellenaba a mano desde mayo 2026. Ahora deploy/pull-web-analytics.py hace todo el pull contra la GA4 Data API. Validado contra junio publicado al dígito: sesiones, form views, landings, navegación, generate_lead, los dos ratios de conversión, pageviews, nuevos usuarios y bounce rate coinciden exactamente, y los 12 canales también. La serie sigue siendo comparable.

Las sesiones se publican como SUMA POR HOSTNAME, que es la agregación con la que se publicaron mayo y junio. Una sesión que toca www y blog cuenta en los dos hosts, así que esa suma es mayor que la consulta sin dimensión: en junio 11.720 sumando hosts contra 11.431 sin dimensión (2,5%). El campo hero.sessions_dedup trae la segunda para que la diferencia sea auditable. Los ratios de conversión usan la suma, para no romper la serie.

Ya no aplica el bug de filtros AND+OR de Supermetrics GAWA: el script va directo a la Data API y los filtros compuestos funcionan (andGroup con inListFilter).
Tres guardas automáticas nuevas
GuardaQué detectaPor qué
Reparto entre hostsEl MoM de los hosts incluidos no va en la misma dirección que el de TODA la property, o difieren más de 15 ppPublicar solo www+blog+know deja un agujero: si el tráfico se mueve de app a www, el canal enseña crecimiento sin que llegue nadie nuevo. Pasó en julio 2026.
Contraste CRMgenerate_lead contra el SQL de demand de HubSpot, con MoM de los dosgenerate_lead es un evento de GA4 y depende del consentimiento y de GTM. En julio 2026 cubre el 35,8% del SQL que registra el CRM.
Form landings concentradosUn solo canal con más del 50% de las llegadas directas al formularioMismo patrón que la anomalía de Display de mayo 2026 (733 sesiones), dada por corregida en junio (96) y reaparecida en julio (397, el 72%).
Pendiente: registrar type_form como event-scoped custom dimension en GA4 PRO (304414381). Hasta que se registre, el split SQL/MQL no es consultable vía API. Acción: Gabi (GA4 admin).
Lo único que sigue a mano: sql_form_submissions y sql_form_completion_rate_pct vienen de HubSpot Forms y su API exige el scope form-analytics-api-access, no concedido. Se pasan con --sql-submissions y --sql-completion-rate; si no se pasan quedan a null con fila de salud en warn. Su base tampoco es la de generate_lead (submission de HubSpot contra evento de GA4), así que no se comparan en el mismo ratio.
Contrato de datos (schema v1.1)
meta { period, window, source, hostname_filter, north_star, note, form_pages, sessions_note, automated_since, automation_note, forms_note, refreshed_at, schema_version } hero { sessions (suma por host), sessions_dedup, form_views, form_landing_sessions, form_nav_views, generate_lead, conv_rate_sessions_pct, conv_rate_form_pct, pageviews, new_users, bounce_rate_pct, sql_form_submissions, sql_form_completion_rate_pct } prev_period { label, window, sessions, form_views, generate_lead, pageviews, new_users, bounce_rate_pct } crm_contrast { sql_demand, sql_talento, sql_qa_excluidos, definition, by_source:[ { source, sql } ], prev:{ label, sql_demand } } channels [ { channel, sessions, generate_lead, conv_rate_pct } ] sessions_by_host [ { host, sessions } ] hosts incluidos excluded_hosts [ { host, sessions, generate_lead } ] app, academy y demás form_landings [ { page, channel, sessions } ] form_views_by_page [ { page, views } ] funnel [ { step, sub, value, pct_of_sessions } ] health [ { scope, signal, value, status } ]
🎯
Eventos
North star: Asistentes
Manual
Conexión
Ninguna. JSON editado manualmente.
Output
data/events/monthly/YYYY-MM.json
Proceso mensual
  1. Recopilar datos de eventos del mes: asistentes, tipo (online/presencial), leads generados.
  2. Editar o crear data/events/monthly/YYYY-MM.json con el schema correspondiente.
  3. Actualizar data/events/monthly/manifest.json.
  4. Actualizar data/hub-summary.json: clave months.{YYYY-MM}.channels.events = total asistentes (estructura mensual).
  5. Deploy manual: wrangler pages deploy .
Canal 100% manual sin dependencias externas. No hay credenciales que gestionar.
🎯
Outbound · TheirStack
North star: Aprobadas · 4 CSVs mensuales
Manual
Fuente
TheirStack (job signals) + clasificador IA (Python)
4 archivos CSV por ciclo mensual
Mercados
midmarket · enterprise · international
Países: ES · PT · IT · UK
Script de pull
deploy/build-outbound.py con los 4 CSVs como argumentos. Metodología: mes natural + aprobadas fechadas por proxy discovered_at (los exports no traen fecha de aprobación).
Output
data/outbound/monthly/YYYY-MM.json
Los 4 archivos CSV
ArchivoQué contieneColumnas clave
TheirStack Leads.csv Todas las señales de hiring detectadas por TheirStack (raw payload JSON en cada fila) id_internal, source, raw_payload
TheirStack Aproved.csv Señales que pasan el filtro IA sobre empresas ya existentes en CRM. La IA decide el canal y prioridad. Se rutean al SDR correspondiente. job_id, canal_target, job_title, job_url, company_name, country_code, decision_ia, motivo_ia, hs_status
TheirStack Aproved New.csv Señales que pasan el filtro IA sobre empresas nuevas (no existen en CRM). Se crean empresa + contacto en HubSpot. Job_ID, Job_Title, Empresa, HS_Company_ID, HS_Contact_ID, SDR_Asignado, Cluster, URL_Oferta, Canal_Slack
TheirStack Discard.csv Señales que no pasan el filtro IA con el motivo de descarte. job_id, empresa, puesto, motivo_descarte, fecha, link
Proceso mensual
  1. Descargar los 4 CSVs del mes desde la plataforma TheirStack / carpeta compartida.
  2. Abrir una sesión de Claude Code en el directorio del repo (/Users/alfonsocalero/02_reporting/shakers-reporting).
  3. Adjuntar los 4 CSVs al contexto de la conversación con @path/to/file.csv.
  4. Indicar el mes del reporte (ej. mayo 2026) y el mes MoM (ej. abril 2026).
  5. Claude extrae métricas con Python (dedup por job_id, cross-tabs mercado/país, motivos de descarte, distribución CRM).
  6. Claude genera data/outbound/monthly/YYYY-MM.json con el schema v1.0.
  7. Claude actualiza manifest.json, hub-summary.json y hace deploy.
Deduplicación: los CSVs Aproved pueden contener duplicados (mismo job_id, diferente motivo_ia). Siempre deduplicar por job_id antes de contar. En Mayo 2026: 248 filas → 234 job_ids únicos.
Clasificación de mercados: viene del campo canal_target del CSV Aproved (midmarket-outbound-team, enterprise-outbound-team, international-outbound-team). Las empresas nuevas (Aproved New) se clasifican por el campo Cluster (UK, ITALIA, PORTUGAL) — todas se cuentan como international.
MoM: en la primera edición (Mayo 2026) prev_period.* = null. A partir de Junio 2026 el JSON del mes anterior ya existe y Claude puede calcular deltas automáticamente.
Motivos de descarte conocidos
CódigoDescripción
crm_prospección_recienteLa empresa fue contactada recientemente — evita saturar
competidor_recruiterLa empresa es una agencia de recruiting / competidor directo
no_techEl rol no es técnico (Product Marketing, Operations, etc.)
crm_descartada_recienteLa empresa fue descartada del CRM recientemente
hibrido_sin_freelanceLa oferta pide híbrido/presencial sin modalidad freelance
outside_ir35UK: contrato outside IR35 (incompatible con modelo Shakers)
junior_internPerfil junior o becario
otrosOtros motivos
Contrato de datos (schema v1.0)
meta { period, window, source, markets, countries, note } hero { signals_total, ai_reviewed, approved_total, approved_existing, approved_new, discarded, approval_rate_pct, ai_review_rate_pct } prev_period { label, window, signals_total?, approved_total?, approved_existing?, approved_new?, discarded?, note? } funnel [ { step, sub, value, pct_of_signals, color } ] by_market [ { market, approved_total, approved_existing, approved_new, pct_of_approved, countries } ] by_country [ { country, flag, approved_total, approved_existing, approved_new, pct_of_approved } ] crm_status [ { status, count, note } ] discard_reasons [ { reason, count, pct, label } ] top_discarded_companies [ { company, count, note } ] health [ { scope, signal, value, status } ]
Brand · Branded Search + Direct
North star: Branded clicks · GSC + GA4
Auto
Fuentes
GSC (sc-domain:shakersworks.com, SA clawdbot) · queries con tokens shaker/shacker/sakers
GA4 PRO canal Direct (SA calero-marketing-analytics), hostnames www+blog+know
Script
python3 deploy/pull-brand.py --month YYYY-MM
Cero pasos manuales. Env: GOOGLE_APPLICATION_CREDENTIALS (GSC) + GA4_SA_FILE + SUPERMETRICS_API_KEY.
Output
data/brand/monthly/YYYY-MM.json
Nota
La definición brand es superset de la del canal SEO (incluye misspellings): los números difieren un poco a propósito.
Brand de pago (desde agosto 2026)

El canal medía solo el brand orgánico, así que una caída era ilegible: no se podía distinguir entre la marca se busca menos y el anuncio de marca se está llevando el clic que antes llegaba gratis. Ahora pull_paid_brand() trae clics, impresiones y gasto de las campañas de Google Ads con brand en el nombre (cuenta 3453900141, vía Supermetrics AW) y el JSON publica paid_brand, hero.total_brand_clicks y hero.organic_share_of_brand_pct.

Serie Ene-Jul 2026: 5 de 6 cambios mensuales van en sentido inverso entre pagado y orgánico, la correlación de niveles es -0,67 y el brand total apenas se mueve (media 4.571 clics, variación 12,5%). En abril→mayo el intercambio es casi 1:1 (orgánico -405, pagado +411). Es el patrón que se espera de canibalización, pero con 7 meses no alcanza significación estadística: es correlación, no causalidad. Para decidirlo hay que pausar el brand de pago un periodo o montar un holdout geográfico. La fila de salud lo levanta automáticamente cada mes que los dos se muevan en sentido inverso.
El brand total es un constructo. GSC y Google Ads son sistemas de medición distintos: la suma orienta pero no es exacta y no debe presentarse como un total auditado.

Se consulta aquí y no se lee del JSON de Google Ads porque ese canal no guarda clics por campaña (solo publica lo necesario para SQL y CPSQL, por decisión de diseño).
La tarjeta del hub: los KPI de siempre siguen ahí

Añadir el brand de pago no cambió ninguna métrica que ya existía. branded_clicks y direct_sessions se calculan con la misma consulta a GSC y a GA4 que antes, con las mismas definiciones, y la ficha los mantiene como los dos primeros KPI. Lo nuevo va en una fila aparte que se oculta si el mes no trae paid_brand.

Lo que sí se rompió, y está corregido el 2026-08-03: la tarjeta del hub había pasado a mostrar Branded clicks + % orgánico del brand, dejando fuera el tráfico directo que el equipo venía siguiendo. Y peor: el MoM de una cuota se estaba imprimiendo como porcentaje relativo, así que 50,4% → 40,5% aparecía como -20% en vez de -9,9 pp. Dos números distintos (-22% de clics y -20% de cuota) que parecían medir lo mismo. Arreglado en index.html: cualquier KPI con fmt:"pct" muestra el delta en puntos porcentuales salvo que traiga delta_pp:false. Afectaba también a Bounce rate, Tasa de aprobación de outbound, Share of voice y Eng. rate. La tarjeta de brand vuelve a llevar los tres: Branded clicks, Tráfico directo y % orgánico del brand.
Direct está contaminado
direct_sessions se usa como proxy de notoriedad, pero el fallo de atribución documentado en docs/tracking.html hace que parte del tráfico de campaña aterrice como Direct al perderse los parámetros antes del consentimiento. En julio 2026 Direct marca máximo de la serie (5.047 sesiones, +13,7%) y eso no se puede leer como más notoriedad de marca. La fila de salud está en warn a propósito.
Contrato de datos (schema v1.1)
meta { channel, period, window, north_star, source, brand_definition, direct_definition, paid_brand_definition, refreshed_at, schema_version } hero { branded_clicks, branded_impressions, branded_ctr, branded_position, brand_share_clicks_pct, direct_sessions, direct_share_pct, paid_brand_clicks, paid_brand_spend, total_brand_clicks, organic_share_of_brand_pct } prev_period { label, window, branded_*, direct_*, paid_brand_clicks, paid_brand_spend, total_brand_clicks } paid_brand { clicks, impressions, spend, ctr, cpc, campaigns:[ { name, spend, clicks, impressions } ] } top_queries [ { q, clicks, impressions, ctr, position } ] top 15 health [ { scope, signal, value, status } ]
Los meses sin paid_brand (antes de agosto 2026, si se repullean sin la clave de Supermetrics) esconden los KPI y el panel de pagado en la ficha, y suben una fila de salud en watch. La serie completa Ene-Jul se rellenó el 2026-08-03.
🤖
AEO · Answer Engine Optimization
North star: Citation SoV · tool propia desde julio 2026
Manual
DOS SERIES QUE NO SE COMPARAN. El canal cambió de metodología en julio 2026 y cada mes declara a cuál pertenece en meta.series.

legacy (mayo-junio 2026, congelada): citas de IA de Bing Webmaster Tools por export manual + panel HubSpot AEO beta.
tool (desde julio 2026): herramienta propia sobre una base de prompts ponderada y actualizada semanalmente, con control de crecimiento. Mide google_aio y LLMs.

Encadenarlas publicaría un -67% en SoV y un +166% en visibilidad de marca que no han ocurrido (junio 15,0% contra julio 5,0%; junio 9,95% contra julio 26,5%). Son universos distintos con los mismos nombres de métrica. La ficha pinta solo los bloques de la serie del período, cambia el banner y el contrato, y la tarjeta del hub lleva prev: null en julio a propósito.
Serie tool (desde julio 2026)
Fuente
Tool propia de AEO. Resumen mensual en markdown (aeo-monthly-YYYY-MM.md) con indicadores, wins, losses, acciones y URLs propias citadas.
Script
deploy/import-aeo-tool.py --month YYYY-MM --export ~/Downloads/aeo-monthly-YYYY-MM.md --total-citations N --universe-citations N
Output
data/aeo/monthly/YYYY-MM.json · schema v2.0
North star
Citation SoV. Es un ratio, así que aguanta el crecimiento de la base de prompts.

Total citations son SOLO citas propias (volumen de Shakers, no del mercado), igual que en la serie legacy, donde Bing WMT reportaba citas de páginas nuestras. El total del mercado va aparte en citations_universe: es el denominador del SoV y sirve para auditarlo, pero no es un KPI porque nadie mueve las citas de la competencia. Aviso: las citas propias también crecen de forma mecánica si crece la base, así que se leen contra la metadata de la base y no en absoluto.
Dos cosas van a mano porque el export no las trae.

1. --total-citations (propias, el KPI) y --universe-citations (todas las marcas, el denominador): el .md solo publica el Citation SoV en porcentaje. El importer valida la coherencia de los tres y sube una fila de salud en warn si la desviación pasa de 0,15 pp, o en watch si falta el universo y el ratio no se puede verificar. En julio: 254 propias / 5.055 del universo = 5,02% contra el 5,0% del export, desviación 0,02 pp.

2. Metadata de la base de prompts: --prompts, --weights-version, --engines y --base-updated. Sin ella no se puede separar una mejora real del crecimiento de la base, que es justo lo que el control de crecimiento de la tool resuelve. Si falta, el JSON lo declara y sube fila de salud en warn. En julio 2026 falta.
Cobertura del detalle de URLs. Las URLs propias citadas del export llegan todas con motor google_aio. Los indicadores de LLM (menciones 55,9%, citas 30,5% en julio) no traen URL, así que la tabla de contenido citable no cubre todo lo que mide la tool. La ficha lo avisa.

Indicadores nuevos que aparezcan en el export y no estén mapeados en INDICATOR_KEYS no se descartan: van a indicators_extra con su etiqueta original y salen en la tabla. Mismo criterio que el bucket otros de supply en LinkedIn.
Serie legacy (mayo-junio 2026, congelada)
Fuentes
Citations = Bing WMT: 3 CSVs mensuales (AIPerformanceOverviewStats, AISearchQueriesReport, AIPageStatsReport), ventana móvil.
HubSpot AEO beta: SoV, visibilidad y sentimiento (lectura manual). Sus citas nunca se usaron como KPI.
Script
deploy/import-aeo.py · congelado, no se ejecuta más. Se conserva para poder regenerar mayo y junio si hiciera falta.
Contrato de datos (schema v2.0 · serie tool)
meta { channel, series:"tool", period, window, mes_cerrado, north_star:"Citation SoV", source, methodology, prompt_base:{ prompts, weights_version, engines, base_updated, note }, comparable_with_legacy:false, legacy_note, engines_en_urls_citadas, export_file, refreshed_at, schema_version:"2.0" } hero { citation_sov_pct, total_citations (SOLO propias · KPI), citations_universe (todas las marcas · denominador, no KPI), brand_visibility_pct } prev_period null hasta agosto 2026 (julio es el primer mes de la serie) indicators [ { key, label, value, help } ] los 8 del export, con su definición indicators_extra [ { label, value, unit } ] indicadores nuevos sin mapear wins / losses [ … ] vacíos si el export dice "sin wins/pérdidas ≥0,5 pp" actions [ … ] acciones sugeridas por la tool cited_urls [ { url, engine, prompt_id, cluster, prompt_family } ] cited_by_cluster [ { cluster, urls } ] derivado de la URL cited_by_prompt_family [ { family, urls } ] h2 = rol, ge = genérico, br = marca health [ { scope, signal, value, status } ]
🗣️
Social Selling · fuente scraper de menciones
deploy/import-social-selling-scraper.py · evaluado el 2026-08-03
Manual
JULIO 2026 NO ESTÁ PUBLICADO, a propósito. El scraper de menciones no puede medir el north star del canal y sin él el mes no se cierra. Decisión de Alfonso (2026-08-03): esperar al tracking del programa en vez de publicar un mes cojo.
Qué cubre el scraper y qué no
Métrica¿Está en el xlsx?Julio 2026
Menciones del mesSí, hoja Menciones, una fila por post73 · 60 autores únicos · 51 perfiles y 22 empresas · 11 reposts
Engagement de las menciones2.517 reacciones · 316 comentarios · 103 compartidos
Comentarios sobre las mencionesSí, hoja Comentarios232 de 204 personas distintas (solo recuento, ver datos personales)
Empleados posteando (north star)NO de forma fiablehay que pedirlo al equipo
Posts de empleadosNO de forma fiablehay que pedirlo
Impresiones estimadasNO existe el campojunio publicó 311.363 con la Matriz de Impresiones
Programa InfluShakers: activos y puntosNOjunio: 53 activos de 108
Por qué la hoja "Post equipo" no vale como north star. Tiene 397 filas desde 2017 y 65 autores, pero cruzada contra las 102 URLs de LinkedIn del roster de la hoja Equipo solo engancha 2 personas en julio 2026, con 4 posts que además son los cuatro reposts. En junio engancha 6 personas y 20 posts, contra las 15 personas y 41 posts que reportó el tracking del programa. Y está contaminada: 10 de sus 14 filas de julio son de fuera del roster, incluida la cuenta de empresa y terceros sin relación. Es un scrape parcial, no el tracking.

El cruce con el roster está comprobado y funciona, pero no se ha integrado: decisión de Alfonso de quedarse solo con menciones agregadas, sin distinguir empleado de tercero.
RUPTURA DE FUENTE. Las menciones de mayo y junio 2026 salen del xlsx manual del equipo (105 en junio) y desde julio del scraper (73). Misma métrica, fuentes distintas: el importer lo declara en meta.source_break_note y sube una fila de salud. No cruzar el MoM sin decirlo, que es el problema que hubo que separar en AEO.
Datos personales
La hoja Comentarios trae nombre, cargo y URL de perfil de 204 personas externas. El importer publica solo recuentos de esa hoja: ni nombres, ni cargos, ni URLs de perfil. Verificado en la salida.

De las menciones sí se publica el autor y la URL del post, que es contenido público que menciona a la marca, pero nunca la URL de perfil de una persona. Ojo igualmente: el 69,9% de las menciones de julio son de perfiles individuales, así que la tabla de top menciones enseña nombres de personas externas. Si eso no encaja, la alternativa es publicar solo el tipo de autor y el engagement.
Cómo cerrar el mes cuando llegue el dato
python3 deploy/import-social-selling-scraper.py \ --xlsx "~/Downloads/menciones_shakers_linkedin.xlsx" --month 2026-07 \ --employees-posting N --posts N --impressions-est N \ --influshakers-active N --influshakers-roster N
Sin esos flags el importer marca el mes como PARCIAL y se niega a escribir: solo lo deja inspeccionar con --stdout. Es la guarda que impide publicar un mes sin north star.

El importer viejo (deploy/import-social-selling.py) sigue siendo el de la fuente manual y no se toca: lee otras hojas y otro formato.
📤
Outbound · cambios del 2026-08-03
deploy/build-outbound.py · normalización de motivos, actividad de SDR y guardas nuevas
Semi
Motivos de descarte: 33 etiquetas para 8 categorías
El clasificador de IA escribe unas veces un slug y otras prosa libre, así que el mismo motivo llegaba partido. En julio 2026: crm_prospección_reciente (124) y crm_prospeccion_reciente sin tilde (17) se contaban por separado, y "Rol no técnico" venía en cuatro variantes.

Ahora canon_reason() agrupa por palabras clave en orden y conserva el texto original en raw_labels, que la ficha muestra debajo de cada motivo. Efecto en el ranking de julio: CRM en prospección reciente pasa de 124 (48,6%) a 141 (55,3%) y Rol no técnico de 21 en cuarto puesto a 53 (20,8%) en segundo. Arreglar el prompt del clasificador para que emita solo slugs y esto deja de hacer falta.
Tres guardas nuevas
GuardaQué detectaJulio 2026
Hueco de revisiónSeñales que entran y no reciben ni aprobación ni descarte. Salta si pasa del 25%422 de 935 (45,1%)
Doble decisiónMismo job_id presente en el export de aprobadas Y en el de descartes. Es una contradicción del origen, no un cálculo: cuenta en los dos lados del embudo6 ofertas
Enrutado de alertasAlertas que hay que reasignar porque el SDR asignado no es el owner activo. Salta por encima del 10%96 de 562 (17,1%) y 104 (18,5%) con owner desconocido
Los dos CSV nuevos
TheirStack Activity SDR.csv entra como bloque ACUMULADO, no del mes. No trae fecha ni job_id, así que no hay forma de recortarlo al período. Se publica con sdr_activity.scope = "acumulado", un aviso en la ficha y una fila de salud que lo repite. No entra en ningún KPI del mes. Flag --activity.

Para poder atribuirlo a un mes hace falta que el export incluya la fecha de la alerta.
TheirStack BD's.csv NO se ha integrado. Comprobado: de sus 364 job_id solo 5 aparecen en Leads y 3 o 4 en aprobadas o descartes, así que no pertenece al embudo que mide este canal (TheirStack → clasificador IA → aprobar o descartar). Es otra vía y meterlo en el mismo embudo daría un total falso. Pendiente de saber qué representa antes de decidir si es un canal aparte o una fase nueva.
Fechado de las aprobadas
Los exports de aprobadas no traen fecha: se fechan por proxy con el discovered_at del job en Leads (join por job_id). Validado: el script reproduce junio 2026 publicado al dígito (841 señales, 289 + 75 aprobadas, 230 descartes) partiendo de los exports acumulados de agosto.

Alternativa comprobada por si el discovered_at falla algún mes: el job_id de TheirStack crece con la fecha de proceso y los cortes entre meses son limpios (julio 2026 = 746.148.801 a 785.346.278). Clasificar por umbral de job_id acierta 655 de 655 contra las fechas reales de los descartes.
👥
Organic Social · degradación por plataforma
Cambio del 2026-08-03 en deploy/pull-social.py
Auto
Un fallo de fuente se publicaba como CEROS. _sm_call() devolvía lista vacía al fallar y las sumas daban 0, con meta.status fijo en "OK". En julio 2026 las seis consultas de Instagram fallaron con QUERY_AUTH_NOT_FOUND y el pull escribió reach 0, profile views 0, interactions 0: la ficha habría publicado un -100% que no ha ocurrido.
Cómo degrada ahora
SituaciónQué hace
Todas las consultas de una plataforma fallanSus KPI pasan a null (no a 0), listas a vacío, bloque.status = "SIN_DATO" y una fila de salud en error con el motivo y la consulta que falló
Alguna plataforma caemeta.status = "PARCIAL" + meta.platforms_sin_dato + meta.status_note
Consolidado del mesExcluye las plataformas sin dato de by_platform, recalcula impresiones, interacciones y engagement rate, y añade platforms_excluidas más un aviso en formula_note: no es comparable con un mes completo
Las cuatro fichas que leen el JSONBanner amarillo arriba (renderNoData()) explicando que los guiones son falta de dato y no una caída. En la ficha de la plataforma caída da el motivo concreto; en las demás, la nota del mes
Julio 2026 está PARCIAL. Instagram sin dato. Para arreglarlo hay que volver a autorizar la fuente IGI en Supermetrics (Settings → Data sources → Instagram Insights). Ojo: el histórico no se recupera solo, hay que repullear julio en cuanto la autorización esté de vuelta.

El label del período en el manifest y en la tarjeta del hub llevan el sufijo · sin Instagram para que no haya que abrir la ficha para saberlo.
🗣️
Influshakers · Social Selling
North star: Empleados posteando · 100% manual
Manual
Fuente
xlsx del equipo ("REPORT SOCIALSELLING <Q>.xlsx"): hoja de menciones por mes + puntuaciones InfluShakers.
Los KPIs hero (empleados posteando, nº posts) NO salen del xlsx: los reporta el equipo desde el tracking del programa y van por CLI.
Script
deploy/import-social-selling.py --xlsx ... --month YYYY-MM --sheet "Menciones LinkedIn <Mes>" --prev-sheet ... --employees-posting N --posts N --prev-employees N --prev-posts N
Output
data/social-selling/monthly/YYYY-MM.json
Mejora pendiente
Pedir al equipo que el xlsx incluya empleados/posts por mes para eliminar los parámetros manuales.
📰
PR · Clippings de prensa
North star: Clippings · 4 trackers de agencia
Manual
Fuentes (4 xlsx, formatos distintos)
The Square (PT: fecha, medio, AAV €, reach) · Wildcom (ES: mes en texto, solo estado "Publicado") · Shared report (IT: lancio desde feb) · Roadmap (FR: retombées desde jun, audience + equiv pub).
En proceso de unificación en un sheet único consensuado con las agencias.
Script
deploy/import-pr.py --month YYYY-MM --square ... --wildcom ... --italy ... --france ...
Normaliza fechas (corrige typos de año), AAV con separadores raros ("7 201 €") y meses en texto.
Regla de publicación
Un mes solo se publica si TODOS los trackers activos están al día. Actividad: PT+ES desde enero · IT desde febrero · FR desde junio. Si falta uno, el mes no existe en el canal.
Output
data/pr/monthly/YYYY-MM.json
📖
Historias del mes
Síntesis narrativa del cierre · no es un canal de datos
Manual

No ingiere datos ni produce JSON. Es la capa de síntesis narrativa encima de los KPIs que el hub ya publica: cada historia es un HTML scrollytelling autocontenido en stories/YYYY-MM.html que cuenta el cierre del mes con el sistema de marca Shakers. Se incluye aquí porque su fuente de datos sí es un proceso de ingesta (el Executive Marketing report de Notion), aunque el output no sea una ficha de canal.

Fuente
Executive Marketing report de Notion (solo lectura) + los JSON del propio hub. Cifras verbatim, nada inventado.
Output
stories/YYYY-MM.html
Autocontenido: tipografía, logo e imágenes embebidas en base64, sin depender de shared/.
Enganche desde el home
CTA animada ("Cuéntame la historia de <mes>") gated por el array STORY_MONTHS en index.html; la flecha se alinea bajo el chip del mes.
Sistema
skill shakers-theme + agente brand-reviewer. IBM Plex Sans, charts a mano en CSS/SVG.
Proceso por mes
  1. Extraer los KPIs del mes del Executive Marketing report de Notion. Cifras exactas, sin redondeos ni datos inventados.
  2. Generar stories/YYYY-MM.html con la skill shakers-theme y pasar el brand-reviewer antes de dar por buena la pieza.
  3. Añadir el slug del mes al array STORY_MONTHS en index.html para que aparezca la CTA en el chip correspondiente.
  4. Commit + npx wrangler pages deploy . --project-name shakers-reporting --branch main + push.
Reglas de datos. El GMV por canal no es aditivo: cada canal atribuye el mismo deal por su propia lente, así que no se suman. Las imágenes generadas con Nim son solo conceptuales, sin cifras (los modelos de imagen distorsionan el texto), y todo meme con números exactos se hace en SVG a mano.
Primera historia: stories/2026-07.html (cierre de julio 2026). Detalle de la capa en README.md y en Roadmap.