CRM & Data
Modeling Semarize Outputs in Your Database or Warehouse
This is the vendor-neutral reference for how to store, version, and query structured conversation signals — regardless of whether you use Snowflake, BigQuery, PostgreSQL, or any other platform. Every platform-specific guide links back here for the shared modeling concepts.
What this guide covers
- The recommended 3-layer storage approach for conversation intelligence data
- Versioning strategies so you never lose the ability to compare scoring frameworks
- Knowledge grounding lineage fields to persist for traceability
- Retention guidance for raw transcripts, evidence excerpts, and structured signals
- Anti-patterns that cause pain at scale
Storage architecture
The 3-Layer Storage Approach
Conversation intelligence data flows through three distinct layers. Each layer serves a different purpose and has different retention, mutability, and query characteristics.
Layer 1: Raw Run Log (append-only)
Every evaluation run produces a raw output. Store this as-is, append-only, and never modify it. This layer is your insurance policy - if your scoring logic changes, you can always re-derive signals from the raw output. In practice this means one row per run, with the full JSON response, transcript reference, timestamps, and run configuration.
- One row per evaluation run (not per signal)
- Full JSON response from the evaluation engine
- Transcript reference (inline text or object storage pointer)
- Run configuration: which kit, which bricks, any custom parameters
- Append-only - never update or delete rows in this layer
Layer 2: Signal / Event Table (row-per-signal, versioned)
Extract individual signals from the raw output and store them as structured, typed rows. Each row represents one signal from one evaluation of one conversation. This is where most queries and joins happen. Always include the scoring version so you can filter and compare across framework iterations.
- One row per signal per evaluation (e.g., overall_score, risk_flag, competitor_mention)
- Typed columns: signal_name, signal_value, signal_type, confidence
- Versioned: scoring_version or kit_version identifies the framework that produced it
- Linked to raw layer via run_id or call_id + evaluated_at
- This is your primary query and join layer
Layer 3: Consumption Views (latest snapshot, optional wide marts)
Create views or materialized tables that present the latest signal values per conversation. These power dashboards, CRM syncs, and downstream applications. They're derived from Layer 2 and can be rebuilt at any time.
- Latest-value views: one row per conversation with the most recent score for each signal
- Wide marts: pivot signals into columns for BI tools that prefer wide tables
- CRM sync views: filtered subsets ready to push back to Salesforce, HubSpot, etc.
- Trend views: time-series aggregations for tracking scoring changes over time
- These are derived - rebuild them whenever the underlying data changes
Versioning
Versioning Best Practice
Scoring frameworks evolve. New bricks get added, evaluation criteria change, weights get adjusted. Without versioning, you lose the ability to compare results across time or explain why a score changed.
Fields to always persist
Never overwrite historical signal rows. Insert new rows with the updated version instead.
Use version columns in WHERE clauses to compare frameworks side-by-side.
When a new version is deployed, backfill a sample of conversations to validate the change before full reprocessing.
Store the version in both the raw run log and the signal table for easy cross-referencing.
Traceability
Knowledge Grounding Lineage
When evaluation logic uses a knowledge base for grounding — product docs, competitive intel, compliance rules — the identity and version of that knowledge base should be persisted alongside the signals it influenced.
Fields to persist (when available)
Data lifecycle
Retention Guidance
Different data types have different retention profiles. Storing everything forever is expensive and often unnecessary. Here's a practical framework.
Raw transcripts
12-24 months in your database, then archive to object storage (S3, GCS, Azure Blob). Keep an object storage pointer in the database so you can retrieve on demand. Compliance requirements may dictate shorter or longer periods.
Evidence excerpts
Keep alongside signal rows indefinitely - they're small (a few sentences each) and critical for explaining why a signal was produced. These are the most-queried part of raw evaluation output.
Structured signals
Keep indefinitely. Signal rows are compact (a few hundred bytes each) and form the basis of all trend analysis, cohort comparison, and framework evaluation. Storage cost is negligible relative to their analytical value.
Raw run logs (full JSON)
Keep for 6-12 months in the database for debugging and reprocessing. After that, archive to object storage. You'll rarely query these directly, but they're invaluable when you need to debug a specific evaluation or reprocess historical data.
What to avoid
Anti-Patterns
These patterns seem convenient at first but create real problems as your conversation intelligence system grows.
Dumping one big JSON blob only
Problem: Storing the entire evaluation response as a single JSON column with no extraction. Queries require JSON parsing on every read, BI tools can't access individual signals without custom transforms, and storage/compute costs balloon because you scan the full blob for every query.
Fix: Extract signals into typed columns in Layer 2. Keep the raw JSON in Layer 1 for reference, but don't make it your query layer.
Wide table as source of truth
Problem: One table with a column for every possible signal (overall_score, risk_flag, competitor_mentioned, next_steps_agreed, ...). Adding a new signal means a schema migration. Sparse columns waste storage. Historical comparisons are impossible because the column didn't exist when older conversations were evaluated.
Fix: Use the row-per-signal model in Layer 2. New signals appear as new rows, not new columns. Create wide views in Layer 3 for BI tools that prefer that shape.
Overwriting scores without version tracking
Problem: Updating signal values in place when the scoring framework changes. You lose all historical context - you can't tell whether a score changed because the conversation was different or because the scoring logic changed. Trend analysis becomes meaningless.
Fix: Never overwrite. Insert new rows with the updated scoring_version. Use consumption views to show the latest value while preserving the full history.
Skipping the raw layer
Problem: Only storing extracted signals without keeping the raw evaluation output. When your scoring logic improves (and it will), you have no way to re-derive signals from historical conversations without re-fetching and re-evaluating every transcript.
Fix: Always persist the raw run log. Storage is cheap. The ability to reprocess historical data is invaluable.
Mixing CRM sync data with analytical data
Problem: Using the same table for both CRM sync (latest values) and historical analysis. CRM syncs need the most recent score; analysis needs the full history. Trying to serve both from one table leads to either stale CRM data or lost history.
Fix: Layer 2 holds the full history. Layer 3 consumption views serve the latest values for CRM sync. Each layer serves its purpose cleanly.
Implementation
Platform-Specific Guides
The concepts above apply to every platform. For native implementation details — data types, loading patterns, performance tuning, and operational playbooks — see your platform's dedicated guide.
Snowflake
VARIANT, stages, Snowpipe, clustering
BigQuery
STRUCT/JSON, partitioning, GCS loads, Looker
PostgreSQL
JSONB, GIN indexes, upserts, materialized views
Supabase
RLS, Edge Functions, real-time, service role
Amazon Redshift
SUPER, dist/sort keys, COPY, Spectrum
Databricks
Delta Lake, Auto Loader, medallion, Unity Catalog
Azure Synapse
COPY INTO, dedicated/serverless pools, Power BI
Google Cloud SQL
Managed Postgres, Auth Proxy, Cloud Scheduler
FAQ
Frequently Asked Questions
Explore