intel-487218.datawarehouse

Intel Data Platform Guide

Intel Data Platform Guide

Who this is for: analysts, researchers, and team members who want to use the data we collect — where it lives, how it gets there, when it refreshes, and how to query it. No code access required.

This file is the source of truth. The Markdown file docs/data/data_guide.md is the source; the online page at datadocs.hml.st is generated from it automatically by CI on every push to main. Edit here, open a PR — CI renders and publishes. See How this page is built.


Overview

Everything we collect — social media, TV, radio, online news — plus everything we derive from it — narratives, subjects, events, briefs — lives in two stores:

Store What it holds How you query it
Google BigQueryintel-487218.datawarehouse Raw collected content (posts, articles, transcripts) and analytics time-series (daily mention/volume tables) SQL, directly (this is the analyst-facing store)
PostgreSQL (Render-managed app DB) Source-of-truth catalogs and definitions (POIs, narratives, subjects), analysis results surfaced in the app (events, briefs, alignment), plus auth & ops Through the app UI / API; direct SQL is for engineers

Rule of thumb: if you want to analyze content or trends, you want BigQuery. If you want the curated definitions and finished results the product shows, that's PostgreSQL — reachable via the app at hml.st or the API.

The data moves through four layers: Collection → Storage → Processing → Presentation. The full diagram is in docs/diagrams/data-flow.txt.


Architecture diagrams

Two diagrams live in docs/diagrams/: end-to-end data flow (Collection → Storage → Processing → Presentation, plus detailed Publishers/messaging-funnel and Events sub-pipelines) and deployment topology (Render vs GCP, schedulers, workflows, jobs, secrets, external APIs). Click either image to open the full-size SVG.

Refreshed 2026-08-13 to match the ScrapeCreators migration, the official X API / col:5x frequent-collection paths, and the Pixel pipeline. If this guide and the diagrams ever drift again, the tables below in this guide are the current source of truth — diagrams are refreshed on request, not on every change.

Data flow diagram: Collection (Cloud Run ingest jobs) to Storage (BigQuery) to Processing (analysis pipelines) to Presentation (API, UI, MCP, /cc agent), plus Publishers/messaging-funnel and Events sub-pipeline detail
Data flow — source: data-flow.txt · excalidraw
Deployment diagram: Render (frontend, backend, Postgres) and Google Cloud (Cloud Scheduler, Workflows, Cloud Run Jobs, BigQuery, Secret Manager), plus external APIs
Deployment — source: deployment.txt · excalidraw

Ingestion (Collection)

Scheduled Cloud Run Jobs (region us-west1) pull from each source and write raw rows to BigQuery. POI handles to track come from Postgres (pois). No ingest job writes to Postgres — all raw data lands in BigQuery.

Source Collected via Entrypoint Cadence Lands in (BQ)
Twitter / X XPOZ API backend/xpoz_etl.py (ingest-tw; no standalone scheduler — runs inside collection-analysis) daily 12:00 (+ Fri 05:00, within collection-analysis) twitter_*
Twitter / X (official API) official X API backend/x_api_etl.py (ingest-x-api, col:x-api-tagged POIs, incremental via since_id) daily 12:00 (+ Fri 05:00, within collection-analysis) twitter_tweets
Twitter / X (col:5x, XPOZ) XPOZ API backend/xpoz_etl.py (ingest-tw-5x, --tags col:5x --exclude-tags col:x-api; serialised with ingest-ig-5x, skips while ingest-tw runs) 00:00, 08:00, 16:00, 20:00 (via collection workflow) twitter_tweets
Twitter / X (col:5x) official X API backend/x_api_etl.py (ingest-x-api-5x, --all-tags col:5x col:x-api) 00:00, 08:00, 16:00, 20:00 (via collection workflow) twitter_tweets
Instagram XPOZ API backend/ig_etl.py daily (after Twitter) ig_*
Instagram (col:5x) XPOZ API backend/ig_etl.py (ingest-ig-5x, --tags col:5x; serialised with ingest-tw-5x) 00:00, 08:00, 16:00, 20:00 (via collection workflow) ig_*
TikTok ScrapeCreators backend/tiktok_etl.py (ingest-tiktok; also downloads each video's audio and transcribes it to Hebrew — see the TikTok section below) daily tiktok_*
TikTok (col:5x) ScrapeCreators backend/tiktok_etl.py (ingest-tiktok-5x, --tags col:5x --scrape-since-hours 16) 00:00, 08:00, 16:00, 20:00 (via collection workflow) tiktok_*
Facebook ScrapeCreators backend/fb_etl.py (ingest-fb + ingest-fb-publishers) daily fb_pages, fb_profiles, fb_posts
Facebook (col:5x) ScrapeCreators backend/fb_etl.py (ingest-fb-5x, --tags col:5x) 00:00, 08:00, 16:00, 20:00 (via collection workflow) fb_pages, fb_profiles, fb_posts
Pixel (disguised venues) ScrapeCreators backend/scripts/pixel_ingest.py (ingest-pixel) daily 06:00 pixel_* (see below)
Telegram web scraper backend/telegram_etl.py daily telegram_*
Telegram (col:5x) web scraper backend/telegram_etl.py (ingest-telegram-5x, --tags col:5x --refresh-days 0) 00:00, 08:00, 16:00, 20:00 (via collection workflow) telegram_*
TV (Channel 14) Google Drive backend/tv_etl.py daily tv_*
Radio (Galei Yisrael) site scraper backend/gly_etl.py daily gly_*
Online papers site scrapers backend/online_paper_etl.py every 2h / daily online_paper_articles
Mainstream papers site scrapers backend/mainstream_papers_etl.py (ingest-mainstream-papers; feeds the events pipeline, separate taxonomy from Online papers above) hourly (within events-pipeline workflow) mainstream_articles
Podcasts RSS + Gemini backend/podcast_etl.py (ingest-podcasts; downloads each new episode's MP3 and transcribes to Hebrew via Gemini) daily 12:00 (+ Fri 05:00, within collection-analysis) podcast_*
Knowledge Base Google Drive backend/kb_etl.py daily kb_*
Mashlat analytics (incident reports) Cloud SQL Postgres (cross-project, mashlat-c2) backend/mashlat_etl.py (ingest-mashlat; read-only analytics.* views, via the Cloud SQL Python Connector) hourly mashlat_*

Each pipeline follows the same pattern: raw content tables, plus <platform>_etl_runs / _etl_run_errors (run audit) and <platform>_*_fetches (delta-window tracking). Raw tables upsert through a transient <table>_staging twin.

Pilot, flag off: X's Activity API (XAA) pushes new posts as they land instead of waiting for ingest-x-api-5x. The x-activity-stream reader holds the single socket and publishes each NDJSON event to Pub/Sub (x-activity-events); it does not MERGE and does not transcribe. x-activity-worker instances MERGE into the shared twitter_tweets table (ingestion_source=x_activity) and then publish the mapped tweet to Pub/Sub topic twitter-live-tweets for other teams. Videos reuse the existing twitter-transcribe topic and service. Gated by Hamal ConfigCat xActivityStream (default off; missing SDK / outage / missing flag => off) — do not set X_ACTIVITY_STREAM_ENABLED=true. Allowlist is POIs tagged camp:red or col:x-api (union) with a twitter_id. Hourly job x-activity-subscription-sync keeps XAA post.create subscriptions matched to that allowlist (ADR 0011). Paid polling via pois.x_api_since_id still runs and stays col:x-api only. Connection health is tracked in x_activity_stream_connections. See ADR 0008.


Processing (Analysis)

Analysis jobs read from BigQuery, load their catalogs (narratives, subjects, POIs, domain knowledge) from Postgres at startup, and write outputs back to both stores.

Pipeline Entrypoint Reads Writes
Narratives backend/analysis_narratives.py BQ raw posts PG narratives (catalog) · BQ mention_daily, narrative_sources, emerging_terms, dashboard_summary
Subjects backend/analysis_subjects.py BQ raw posts PG subjects · BQ subjects_sources, subjects_daily
Events backend/events_pipeline.py BQ mainstream_articles (clustering input) + online_paper_articles/social/TV counts (amplification) PG events, event_sources, event_feedback
Messaging-funnel alignment backend/messaging_funnel_workflow.py BQ twitter_tweets, fb_posts PG post_alignment
Briefs (weekly / daily) backend/weekly_brief_etl.py, daily brief ETL BQ aggregates + PG catalogs PG weekly_briefs, daily_brief
Keyword monitors keyword_monitor_runner.py (on-demand) BQ raw tables PG keyword_monitors, keyword_narratives
D-Day Signs (סימנים מעידים) dday-candidatesdday-judgedday-events, + non-blocking dday-locations BQ raw tables + PG dday_signs/dday_sign_versions (Sign definitions) BQ dday_sign_candidates, dday_sign_detections, dday_detection_locations · PG dday_events, dday_event_detections, dday_event_places

Most analysis uses Gemini for classification, extraction, embeddings, and Hebrew synthesis.

The D-Day Signs pipeline is a three-stage chain — candidate nomination (keyword match), judging (Gemini + grounding check), aggregation into scored events for the /dday/triage queue — documented in full in docs/dday_signs.md (content selection, the judge's grounding rule, event merge/corroboration, authority bands). This section and the tables below cover only where its data lives.


When the flows run

All schedules are Asia/Jerusalem; schedulers live in us-west1 (Cloud Scheduler → Workflows).

Time (IL) What runs
hourly ingest-mashlat (Mashlat analytics — incident reports collection)
every 2h, 06:00–22:00 events-pipeline (mainstream-papers → events analysis, 3h window)
daily 04:00 events-pipeline-daily (7-day rescore window)
daily 06:00 ingest-messaging-briefs (parse desk briefs); ingest-pixel (disguised-venue collection)
daily 07:30 messaging-funnel-workflow (collection → FB publishers → alignment)
every 3h, on the hour dday-signs-trigger: dday-candidatesdday-judgedday-events (+ non-blocking dday-locations) — election-day cadence (every 5 min, dday-signs-trigger-dday) exists but is created paused
00:00, 08:00, 16:00, 20:00 daily collection workflow: ingest-x-api-5x + ingest-tw-5x + ingest-fb-5x + ingest-ig-5x + ingest-tiktok-5x + ingest-telegram-5x, for col:5x-tagged POIs only. With the 12:00 full sweep below, that is five passes a day for these POIs.
daily 12:00 (+ extra Fri 05:00) collection-analysis: all ingest jobs (incl. ingest-tw Twitter) → narratives + subjects analysis → events → weekly & daily briefs

To check the last successful run of any pipeline, query its <platform>_etl_runs table in BigQuery, or use /admin/jobs in the app.


Getting notified when collection finishes (Pub/Sub)

Don't want to poll BigQuery or /admin/jobs? Both collection-analysis and collection publish a Pub/Sub event — with per-source success/failure counts — right after their collection phase finishes (Twitter video transcripts for that run included), before analysis and briefs even start.

Message body (JSON, base64-encoded in the Pub/Sub data field — any client library decodes this automatically):

{
  "workflow": "collection-analysis",
  "status": "partial_failure",
  "timestamp": "2026-08-12T09:03:41Z",
  "summary": {
    "total_sources": 11,
    "sources_succeeded": 10,
    "sources_failed": 1,
    "items_succeeded": 4231,
    "items_failed": 7
  },
  "sources": {
    "tw": { "status": "success", "succeeded": 812, "failed": 0 },
    "kb": { "status": "failed", "succeeded": 0, "failed": 7 }
  },
  "failed_sources": ["kb"]
}

sources.<name>.succeeded/.failed are item-level counts (posts/documents processed vs. failed within that source's own run), not just whether the job process exited 0.

To subscribe, you need roles/pubsub.subscriber on the topic — ask a project developer to grant it to you or your service account (see For admins for the equivalent BigQuery grant pattern; same idea, roles/pubsub.subscriber instead). Once granted:

gcloud pubsub subscriptions create my-subscription \
  --project=intel-487218 \
  --topic=ingestion-completed \
  --topic-project=intel-487218

gcloud pubsub subscriptions pull my-subscription --project=intel-487218 --auto-ack --limit=5

If you're on a different GCP project (same org or not), you don't need access to this repo or project console — ask a project developer to grant your service account subscriber rights on the topic (or set up a push subscription to your endpoint on your behalf); you only need the topic name above and your own gcloud/client-library access to your own project.

For server-side filtering (e.g. only failures, or only one workflow), the full cross-project setup (both "you subscribe" and "we push to your endpoint" patterns), and delivery/idempotency notes, see Subscribing to ingestion-completed — everything above is enough to get started even without access to that file.


Where things live — BigQuery vs PostgreSQL

The same concept often has a definition in Postgres and time-series data in BigQuery. This map tells you which side to query:

Entity PostgreSQL (source of truth) BigQuery
Narratives narratives mention_daily, narrative_sources
Meta-narratives meta_narratives denormalized onto narratives.meta_narrative
Group narratives narrative_groups + narratives same narratives / mention_daily / narrative_sources
Keyword narratives keyword_monitors, keyword_narratives (reads raw tables; no dedicated BQ output)
Subjects subjects subjects_sources, subjects_daily
Events events, event_sources, event_feedback (reads online_paper_articles + counts; no BQ output)
Weekly / daily briefs weekly_briefs, daily_brief
POIs / domain knowledge pois, domain_knowledge
Raw social/news/TV/radio/podcast content twitter_*, tiktok_*, fb_*, ig_*, telegram_*, tv_*, gly_*, podcast_*, online_paper_articles, kb_*
Mashlat analytics (incident reports) mashlat_signals, mashlat_events, mashlat_cases, mashlat_tasks, mashlat_regions, mashlat_teams, mashlat_event_categories, mashlat_signals_files, mashlat_events_files, mashlat_signals_events
Pixel (disguised-venue investigation) pixel_venues, pixel_posts, pixel_venue_snapshots, pixel_lean_daily, pixel_flip_state, pixel_coordination, pixel_ig_* (IG intake)
SocialAgents KPI (external push) socialagents_kpi_snapshot
D-Day Signs (סימנים מעידים) dday_signs, dday_sign_versions, dday_sign_stat, dday_events, dday_event_detections, dday_event_places, dday_event_lineage dday_sign_candidates, dday_sign_detections, dday_detection_locations

BigQuery table catalog

Grouped by source. Key columns are a starting point — the full schema is visible in the BigQuery console.

🐦 Twitter / X

Table Contents Key columns
twitter_users X profiles username, name, description, location, followers_count, verified, gist (AI role summary)
twitter_tweets Tweets (partitioned by created_at day) id, text, author_username, created_at, created_at_date, like_count, retweet_count, reply_count, quote_count, hashtags, reply_settings
twitter_followers follower→followee edges follower_id, followee_id
twitter_user_geocodes geocoded user locations normalized_location, latitude, longitude

reply_settings is only populated for the official-X-API rows (col:x-api-tagged POIs — see the Ingestion table above). It's NULL for the bulk of twitter_tweets, which comes via XPOZ: verified live against XPOZ's API that it silently drops unsupported field names from a fields request rather than erroring, and replySettings/conversationControl are not among the fields it returns — this isn't a config gap, XPOZ just doesn't expose who-can-reply data.

🎵 TikTok

Table Contents Key columns
tiktok_users accounts username, nickname, follower_count, like_count, post_count, is_verified
tiktok_posts videos id, username, description, created_at_date, play_count, like_count, comment_count, share_count, transcript

Videos are transcribed. Every collection pass pulls the audio track off each new video (yt-dlpffmpegGemini, same path as podcasts and FB/X video) and writes three columns: transcript (the text), transcript_language (he-IL), and transcribed_at. This is spoken audio, which is a different thing from description — the caption the author typed. For "what did this person actually say", search transcript; for "how did they frame the post", read description.

Transcription is gated on engagementshare_count×3 + comment_count×2 + like_count, the same weighting the warehouse uses to rank TikTok posts for subjects and narratives, with a default threshold of 250. Low-reach videos are deliberately left untranscribed. This matches how Facebook, X and Instagram already work.

transcript IS NULL therefore means one of three things, and they are worth telling apart: the video was below the engagement gate, or we tried and got nothing back (no speech in the clip), or it hasn't been attempted yet. transcribed_at is the discriminator — it is stamped on every finished attempt, successful or not, so transcribed_at IS NOT NULL AND transcript IS NULL is precisely "we tried, there was nothing to transcribe".

Historical coverage — collected before the gate existed, when everything was transcribed — was ~79% of videos with any text but only ~61% with 100+ characters, median ~430 chars. The gap skews by account type: politicians' own accounts ran 90–96%, short-form news desks much lower (mako.israel 61%, n12news 71%), because so much of their output is captioned b-roll over music with no speech in it. Expect lower volume and a similar hit rate going forward — the gate cuts spend, it does not improve the ratio, because engagement barely predicts whether a clip contains speech.

Use video_duration_ms, not video_duration. The old video_duration column mixed units by collector — milliseconds from ScrapeCreators, seconds from XPOZ and Apify (measured on the same videos: 100.75 where ScrapeCreators said 100750) — so any query over it was wrong for part of the table. video_duration_ms is milliseconds for every row, backfilled from the collector recorded on each row rather than guessed from magnitude. Divide by 1000 for seconds.

⚠️ video_duration is left in place, unit-mixed and no longer written, purely as a rollback path. Rows collected from 2026-08-20 have it NULL. Do not query it.

The same convention holds for fb_posts and twitter_tweets: transcribed_at records the attempt, transcript holds text or NULL. Both carried [empty]/[failed] marker strings for a few days from 2026-08-13; if you are querying historical rows and see those values, treat them as NULL.

📘 Facebook

Table Contents Key columns
fb_pages pages username, name, category, followers_count, page_url
fb_profiles personal profiles username, name, bio, friends_count, location
fb_posts posts (pages + profiles + groups) id, source_type, username, text, permalink, created_at_date, reactions_total, comments_count, shares_count

📸 Instagram

Table Contents Key columns
ig_users accounts username, full_name, biography, follower_count, media_count
ig_posts posts / reels id, username, post_type, caption, like_count, comment_count, video_play_count, transcript, created_at_date

✈️ Telegram

Table Contents Key columns
telegram_channels channels username, title, description, member_count
telegram_messages messages id, channel_username, text, created_at_date, views, forwards, forwarded_from_channel, link_domain

📺 TV (Channel 14) & 📻 Radio (Galei Yisrael)

Table Contents Key columns
tv_documents parsed broadcast transcripts broadcast_date, doc_type, content_markdown, word_count, speakers
tv_segments per-speaker transcript segments file_id, speaker, text, timestamp_str
gly_segments radio program segment headlines program_name, headline, subtitle, broadcast_date, mp3_url
gly_programs program registry program_id, title, description_html

🎙️ Podcasts

Table Contents Key columns
podcast_episodes episode registry (per show, from RSS) episode_id, show_slug, title, audio_url, published_date, duration_seconds, transcript_status
podcast_documents one parsed-episode document (mirrors tv_documents) episode_id, show_slug, published_date, content_markdown, word_count, speakers
podcast_segments per-speaker transcript segments podcast_segment_id, episode_id, show_slug, segment_index, speaker, text

📰 Online papers & 📚 Knowledge Base

Table Contents Key columns
online_paper_articles news articles outlet_slug, title, author, url, published_at, summary, content_text, ai_summary
mainstream_articles events-pipeline's news source (separate taxonomy from online_paper_articles) outlet_slug, title, url, published_at, content_text, gdelt_gkg_id (GDELT's GKGRECORDID, when sourced via GDELT)
kb_documents internal Drive documents file_id, content_markdown, word_count, page_count

🚨 Mashlat analytics (incident reports)

Mirrors Mashlat's read-only analytics.* Postgres views (a separate GCP project, mashlat-c2) for D-Day incident response data — signals from the field, triaged events, cases, and tasks, plus dimension lookups. signals/events/cases/tasks sync incrementally (hourly, via a last_modified watermark); the rest are small dimension/junction tables, fully re-extracted every run. reporter_name/reporter_phone (PII on mashlat_signals) are intentionally not mirrored.

Table Contents Key columns
mashlat_signals raw field reports (app, phone, WhatsApp, patrol, simulation) id, external_id, received_at, source, status, severity_raw, body, region_id, event_id (deprecated — see below), location (GEOGRAPHY), last_modified
mashlat_events triaged incidents id, title, status, severity, category_id, region_id, case_id, due_at, resolved_at, location (GEOGRAPHY), last_modified
mashlat_cases case files grouping related events id, title, summary, status, severity, region_id, last_modified
mashlat_tasks work items on an event id, title, status, priority, event_id, assigned_team_id, due_at, completed_at, last_modified
mashlat_regions geographic/org regions (dimension) id, name, code
mashlat_teams response teams (dimension) id, name, type, region_id, phone
mashlat_event_categories incident category lookup (dimension) id, name, default_severity, sla_minutes
mashlat_signals_files signal↔file attachment links (junction) id, signal_id, file_id, sort
mashlat_events_files event↔file attachment links (junction) id, event_id, file_id, sort
mashlat_signals_events signal↔event links, many-to-many (junction) id, signal_id, event_id, sort

location columns are GEOGRAPHY (point), decoded server-side from the source's PostGIS geometry.

As of 2026-08-11, a signal can link to more than one event (signals.event was retired for a many-to-many relation). mashlat_signals.event_id still exists but now only reflects the earliest-linked event, kept for backward compat — join mashlat_signals_events for the full picture.

📈 Analysis time-series

Table Contents Key columns
narratives frozen 2026-03 — now in PG narrative_id, name_he, meta_narrative, escalation_stage, political_camp
mention_daily daily narrative metrics narrative_id, source_platform, mention_date, volume, engagement, poi_volume
narrative_sources per-post narrative mentions narrative_id, source_item_id, source_platform, mention_date, engagement, raw_engagement, is_poi
subjects_daily daily subject metrics subject_id, mention_date, source_platform, camp, volume, engagement, top_actors, top_posts
subjects_sources per-post subject mentions subject_id, source_platform, author_id, camp, mention_date, engagement
emerging_terms trending terms term, growth_pct, first_seen_date, run_date
dashboard_summary per-run KPIs run_date, total_posts_7d, active_narratives_count

engagement vs raw_engagement (HML-155). Every per-item post query wraps its weighted-interaction formula in LN(1 + ...) — a log-compressed score meant only for ranking one post against another (it stops one viral post dominating a narrative's top-posts list). narrative_sources.engagement holds that ranking score; is_poi ordering and top-posts selection should keep using it. narrative_sources.raw_engagement holds the true, unlogged interaction count and is what mention_daily.engagement sums — summing the log-compressed score instead (as this table did before HML-155) makes mention_daily.engagement track mention_daily.volume almost 1:1, which is what the weekly-brief chart bug looked like. tv/podcast/online_paper have no native engagement signal, so their raw_engagement is always 0 (their engagement stays a constant ranking proxy, not a measurement).

🕵️ Pixel (disguised-venue detection)

Kept deliberately separate from fb_pages/fb_posts: these are investigation targets (suspected sleeper pages), not tracked POIs.

Table Contents Key columns
pixel_venues candidate venues + A1 provenance handle, name, page_url, admin_country, former_names, runs_political_ads
pixel_posts venue posts post_id, venue_handle, text, reactions_total, is_political, stance_json
pixel_venue_snapshots daily follower/like/post-count snapshots handle, snapshot_date, followers_count, likes_count, post_count
pixel_lean_daily daily pro-Likud lean per venue (flip-watch timeline) handle, day, lean, political_posts
pixel_flip_state latest flip verdict per venue (dedup for alerting) handle, flipped, changepoint_day, lean_before, lean_after
pixel_coordination cross-venue co-shared URL/media clusters cluster_id, kind, venues, coordinated, span_minutes, evidence
pixel_ig_profiles Instagram venue intake — profiles username, full_name, biography, follower_count, fb_handle (linked FB venue, if known)
pixel_ig_posts Instagram venue intake — posts post_id, username, post_type, caption, like_count, comment_count, is_political, stance_json
pixel_ig_comments comments on pixel_ig_posts (commenter-overlap analysis) comment_id, post_id, post_username, commenter_username, text, created_at_date
pixel_ig_following who each suspect IG venue follows username, following_username, following_full_name, is_verified
pixel_ig_followers who follows each suspect IG venue (ranked, oldest first) username, follower_username, rank, first_seen_at, last_seen_at

🚨 D-Day Signs (סימנים מעידים)

The Sign detection pipeline's audit trail — see docs/dday_signs.md for what a "Sign" is and how a candidate becomes a detection. Sign definitions (dday_signs, dday_sign_versions) and finished events (dday_events, dday_event_detections) are in Postgres — these BQ tables are the append-only trail behind them.

Table Contents Key columns
dday_sign_candidates stage 01 output — every keyword hit nominated for judging candidate_id, sign_id, sign_slug, platform, item_id, occurred_on, text, matched_keywords
dday_sign_detections stage 02 output — every judged (Sign, item) pair, matched or not detection_id, candidate_id, sign_id, matched, grounded, evidence_quote, confidence, severity (HML-222 phase 3 stopped writing the old urgency column; it is left in place holding history), answered
dday_detection_locations place names extracted/geocoded from matched+grounded detections location_id, content_hash, platform, item_id, place_text, lat, lng, country_code

A candidate is nominated at roughly 50:1 versus detections that actually match — most of dday_sign_candidates is the judge saying no. A row's matched AND grounded is what makes it a real detection; see the file above for why an ungrounded quote is demoted rather than dropped.

Ignore operational tables ending in _etl_runs, _run_errors, _fetches, and _staging — they are internal ETL bookkeeping, not analysis data.


Full BigQuery table inventory

The sections above are the analyst-facing curated view. For completeness, this is every table any backend code defines or writes to in intel-487218.datawarehouse, grouped by the source file that owns its schema. 🗃️ = content/analysis table (covered above); ⚙️ = operational/internal (audit, delta-tracking, cache) — safe to ignore for analysis.

Table Owning file Kind
twitter_users xpoz_etl_bigquery.py / xpoz_etl.py 🗃️
twitter_tweets xpoz_etl_bigquery.py / xpoz_etl.py 🗃️
twitter_followers xpoz_etl_bigquery.py / xpoz_etl.py 🗃️
twitter_user_geocodes xpoz_etl.py 🗃️
tweet_context_cache xpoz_etl.py ⚙️
twitter_tweets_fetches xpoz_etl.py ⚙️
twitter_tweet_interactions_fetches xpoz_etl.py ⚙️
twitter_followers_fetches xpoz_etl.py ⚙️
twitter_following_fetches xpoz_etl.py ⚙️
etl_runs, etl_run_errors xpoz_etl.py (shared Twitter/IG audit) ⚙️
ig_users, ig_posts ig_etl_bigquery.py 🗃️
ig_posts_fetches ig_etl_bigquery.py ⚙️
ig_etl_runs, ig_etl_run_errors ig_etl_bigquery.py ⚙️
ig_enrichment_thresholds ig_etl_bigquery.py ⚙️
tiktok_users, tiktok_posts tiktok_etl_bigquery.py 🗃️
tiktok_posts_fetches tiktok_etl_bigquery.py ⚙️
tiktok_etl_runs, tiktok_etl_run_errors tiktok_etl_bigquery.py ⚙️
fb_pages, fb_profiles, fb_posts fb_etl_bigquery.py 🗃️
fb_posts_fetches, fb_entities_fetches fb_etl_bigquery.py ⚙️
fb_etl_runs, fb_etl_run_errors fb_etl_bigquery.py ⚙️
telegram_channels, telegram_messages telegram_etl_bigquery.py 🗃️
telegram_messages_fetches telegram_etl_bigquery.py ⚙️
telegram_etl_runs, telegram_etl_run_errors telegram_etl_bigquery.py ⚙️
tv_documents, tv_segments tv_etl_bigquery.py 🗃️
tv_files, tv_file_scans tv_etl_bigquery.py ⚙️
tv_etl_runs, tv_etl_run_errors tv_etl_bigquery.py ⚙️
gly_segments, gly_programs gly_etl_bigquery.py 🗃️
gly_etl_runs gly_etl_bigquery.py ⚙️
online_paper_articles online_paper_etl_bigquery.py 🗃️
online_paper_etl_runs, online_paper_etl_run_errors online_paper_etl_bigquery.py ⚙️
mainstream_articles mainstream_papers_etl_bigquery.py (events-pipeline's news source — separate taxonomy from online_paper_articles) 🗃️
mainstream_etl_runs, mainstream_etl_run_errors mainstream_papers_etl_bigquery.py ⚙️
podcast_episodes, podcast_documents, podcast_segments podcast_etl_bigquery.py 🗃️
podcast_etl_runs, podcast_etl_run_errors, podcast_feed_scans podcast_etl_bigquery.py ⚙️
kb_documents kb_etl_bigquery.py 🗃️
kb_files, kb_file_scans kb_etl_bigquery.py ⚙️
kb_etl_runs, kb_etl_run_errors kb_etl_bigquery.py ⚙️
mashlat_signals, mashlat_events, mashlat_cases, mashlat_tasks, mashlat_regions, mashlat_teams, mashlat_event_categories, mashlat_signals_files, mashlat_events_files, mashlat_signals_events mashlat_etl_bigquery.py 🗃️
mashlat_etl_runs, mashlat_etl_run_errors mashlat_etl_bigquery.py ⚙️
pixel_venues, pixel_posts, pixel_venue_snapshots, pixel_lean_daily, pixel_flip_state, pixel_coordination pixel_etl_bigquery.py 🗃️
pixel_ig_profiles, pixel_ig_posts, pixel_ig_comments, pixel_ig_following, pixel_ig_followers pixel_etl_bigquery.py (Instagram intake, added alongside the FB-only Pixel tables above) 🗃️
pixel_ig_posts_fetches pixel_etl_bigquery.py ⚙️
mention_daily, narrative_sources, emerging_terms, dashboard_summary analysis_narratives_bigquery.py 🗃️
camp_hourly_cdf analysis_narratives_bigquery.py (posting-time model for the events amplification baseline) 🗃️
tweet_media_enrichment analysis_narratives_bigquery.py (OCR/vision cache for tweet images) ⚙️
post_entities analysis_narratives_bigquery.py (per-post NER cache for the entity-mentions report) ⚙️
entity_mention_daily analysis_narratives_bigquery.py (per-audience daily entity-mention counts) 🗃️
analysis_narratives_runs, analysis_narratives_run_errors analysis_narratives_bigquery.py ⚙️
subjects_sources, subjects_daily analysis_subjects_bigquery.py 🗃️
analysis_subjects_runs, analysis_subjects_run_errors analysis_subjects_bigquery.py ⚙️
brief_correlation, post_correlation report_correlation.py (cosine-similarity scores linking messaging briefs/items to publisher posts) 🗃️
narrative_centroids, subject_centroids embedding_utils.py (embedding-centroid cache used by the events pipeline to correlate articles to subjects/narratives) ⚙️
dday_sign_candidates dday_candidates_bigquery.py 🗃️
dday_sign_detections dday_detections_bigquery.py 🗃️
dday_detection_locations dday_detection_locations.py 🗃️
x_activity_stream_connections x_activity_stream_bigquery.py (XAA reader connection health; workers MERGE posts into twitter_tweets, flag off) ⚙️

Query these through the app / API rather than direct SQL. Grouped by role (representative, not exhaustive):

Group Tables
Catalogs & definitions pois, narratives, meta_narratives, narrative_groups, subjects, subject_narratives, keyword_monitors, keyword_narratives, domain_knowledge, message_narrative_catalog
Analysis results events, event_sources, event_feedback, post_alignment, weekly_briefs, daily_brief, channel_metrics_daily, kpi_daily_snapshots, socialagents_kpi_snapshot (external webhook push, not a scheduled ETL — POST /webhooks/socialagents), network_analysis_results, network_cluster_summaries
D-Day Signs dday_signs, dday_sign_versions, dday_sign_stat (Sign definitions + calibration), dday_events, dday_event_detections, dday_event_places, dday_event_lineage (triage queue at /dday/triage)
Messaging & audiences audiences, audience_tags, audience_reports, message_brief, message_narrative, message_subtitle, message_item, messaging_item, messaging_item_metric, publisher, publisher_user, publisher_channel, intelligence_report
Product surfaces cc_sessions, cc_messages, cc_config, situation_rooms, situation_room_stories, recommended_items, item_distributions
Auth & ops users, roles, permissions, role_permissions, user_roles, oauth_clients, oauth_codes, oauth_refresh_tokens, api_tokens, operation_costs, pipeline_runs

Accessing the data

BigQuery (analysts)

Your identity is your @hamal-elections.co.il Google account. To query, you need these IAM roles on project intel-487218:

Role Why
roles/bigquery.dataViewer read the tables
roles/bigquery.jobUser run queries
roles/bigquery.studioUser + roles/bigquery.readSessionUser (optional) BigQuery Studio / notebooks

Ask one of the project developers to grant these — see For admins.

Three ways to query:

  1. BigQuery Studio (no install): open https://console.cloud.google.com/bigquery?project=intel-487218, expand intel-487218 → datawarehouse, write SQL, Run.
  2. bq CLI: bash gcloud auth login gcloud config set project intel-487218 bq query --use_legacy_sql=false \ 'SELECT username, followers_count FROM `intel-487218.datawarehouse.twitter_users` ORDER BY followers_count DESC LIMIT 10'
  3. Python: ```python from google.cloud import bigquery

client = bigquery.Client(project="intel-487218") for r in client.query( "SELECT username, followers_count " "FROM intel-487218.datawarehouse.twitter_users " "ORDER BY followers_count DESC LIMIT 10" ).result(): print(r.username, r.followers_count) ```

PostgreSQL

The Postgres DB is the application's operational store. For almost everyone, the right way to read it is through the product:

Direct SQL access to Postgres is reserved for engineers via the Render dashboard.


Example queries (BigQuery)

Top tweets by likes in the last 7 days (filter on the partition column to keep it cheap):

SELECT author_username, text, like_count, retweet_count
FROM `intel-487218.datawarehouse.twitter_tweets`
WHERE created_at_date >= DATE_SUB(CURRENT_DATE(), INTERVAL 7 DAY)
ORDER BY like_count DESC
LIMIT 10;

Daily mention volume of a narrative:

SELECT mention_date, SUM(volume) AS volume, SUM(engagement) AS engagement
FROM `intel-487218.datawarehouse.mention_daily`
WHERE narrative_id = 'NARRATIVE_ID_HERE'
GROUP BY mention_date
ORDER BY mention_date;

Search Telegram messages for a term:

SELECT channel_username, created_at_date, views, text
FROM `intel-487218.datawarehouse.telegram_messages`
WHERE text LIKE '%TERM%'
  AND created_at_date >= DATE_SUB(CURRENT_DATE(), INTERVAL 30 DAY)
ORDER BY views DESC
LIMIT 50;

Top subjects on TikTok yesterday:

SELECT subject_id, SUM(volume) AS volume
FROM `intel-487218.datawarehouse.subjects_daily`
WHERE mention_date = DATE_SUB(CURRENT_DATE(), INTERVAL 1 DAY)
  AND source_platform = 'tiktok'
GROUP BY subject_id
ORDER BY volume DESC
LIMIT 20;

Things worth knowing (gotchas)


For admins: granting BigQuery access

Give a new analyst read-only access with:

gcloud projects add-iam-policy-binding intel-487218 \
  --member="user:NEW_ANALYST@hamal-elections.co.il" \
  --role="roles/bigquery.dataViewer" --condition=None
gcloud projects add-iam-policy-binding intel-487218 \
  --member="user:NEW_ANALYST@hamal-elections.co.il" \
  --role="roles/bigquery.jobUser" --condition=None

# optional — BigQuery Studio / notebooks
gcloud projects add-iam-policy-binding intel-487218 \
  --member="user:NEW_ANALYST@hamal-elections.co.il" \
  --role="roles/bigquery.studioUser" --condition=None
gcloud projects add-iam-policy-binding intel-487218 \
  --member="user:NEW_ANALYST@hamal-elections.co.il" \
  --role="roles/bigquery.readSessionUser" --condition=None

Developers get a broader custom role (intelDev, includes write) via make gcloud-grant-dev-access. Do not give that to analysts — read-only is enough.


How this page is built and published

Preview locally:

uv run --with markdown python docs/data/render.py
open docs/data/site/index.html

Keep it current: run the /update-datadocs command — it re-scans the codebase for schema, source, and schedule changes and updates this file. One-time CI/Access setup is documented in docs/data/README.md.