Push, webhook, and email sources are event streams: data arrives one event at a time, the shape can change between events, and you don’t control the sender’s schema. To keep ingestion reliable, Panels stores every field from these sources as text (VARCHAR). An event is never rejected because a field changed type. Typing happens in a downstream clean step — a small SQL node that casts each column to the type you want. This is the medallion pattern: a raw layer that accepts everything, and a typed layer you build on top.
This applies to event sources only. File uploads and database connectors keep their real column types — see the table below.

Why event fields are text

Panels infers a column’s type from the first event it sees, and a stored column’s type is immutable. If the first event happened to make a field look like a specific type, a later event that doesn’t match would fail forever:
  • The first referrer arrives as an object {"url": "..."} → the column locks to a structured type → a later plain-string referrer can’t be stored.
  • The first device_id arrives as 550e8400-e29b-41d4-a716-446655440000 → it looks like a UUID and locks the column to UUID → a later device_id of "unknown" can’t be stored.
Storing every field as text removes this trap entirely. Text holds any value — numbers, UUIDs, JSON, empty strings — so ingestion can never break on a changing event shape. You decide the real types later, where it’s safe and reversible.

Which sources store text

SourceHow fields are stored
Push / webhook (HTTP ingest)Every field as text
Email (JSON body payload)Every field as text
API / app connectorsScalars keep their inferred type; nested fields (objects, arrays) stored as JSON text
File upload (CSV, Parquet, JSON)Real column types (the file’s schema)
Database / warehouse (PostgreSQL, Snowflake, …)Real column types (the source catalog)
Email attachments (CSV, etc.)Real column types (the file’s schema)
The rule: a source stores text when its schema is inferred from arbitrary event values. A source that carries a real, declared schema (a file header, a database catalog) keeps its types.

Querying text columns

Text columns are fully queryable. Two functions do most of the work:
  • try_cast(col AS TYPE) — convert a value, returning NULL if it can’t (unlike cast, which errors). This is what makes typing safe.
  • json_extract_string(col, '$.path') — pull a value out of a field that holds JSON text.

Create a typed table

Add a SQL node downstream of your event source that casts each column. By convention, name it clean_events — your raw source stays the bronze layer, and this becomes the typed silver layer your dashboards build on.
1

Add a SQL node

Connect a new SQL node to your event source.
2

Cast each column with try_cast

Convert every field you care about to its real type. Anything that can’t be cast becomes NULL instead of breaking the run.
3

Build on the typed table

Point charts, dashboards, and further transforms at clean_events. They get clean types; ingestion stays bulletproof.
Casting bad values to NULL silently drops them. To keep them for inspection, add a column that flags the failures — for example CASE WHEN amount IS NOT NULL AND try_cast(amount AS DECIMAL) IS NULL THEN amount END AS amount_unparsed.

Existing tables keep their types

This text-first behaviour applies to new event tables. If you already have an event source whose columns were materialized as specific types, Panels leaves them as-is — your existing tables and dashboards don’t change. Only a freshly created (or purged and rebuilt) event source lands as text.
After purging and rebuilding an event source, its columns come back as text. Re-point downstream nodes at your clean_events step, not the raw table, so dashboards keep their types.