Skip to main content
Matik Help Center home page Help Center
Matik Blog Case Studies
  1. Matik
  2. Building Templates
  3. Dynamic Content

Using Matik Analytics as a Data Source

Matik Analytics lets you query the usage and engagement data Matik already collects about your enterprise, the same way you query a data warehouse or a CRM. You write SQL against a small set of curated tables, and the results become Dynamic Content or Input options like any other data source.

There is nothing to connect. Matik Analytics is not in the list of data sources you add under Connect Data Source, and it needs no credentials, no host, and no permissions setup. Once it is enabled for your enterprise, it appears in the data source picker on its own.

Every query is automatically scoped to your own enterprise. You never write a filter to limit results to your company's data, and there is no way to see another company's.

Example Use Cases

Report on Matik usage in the format your team already uses

Instead of exporting a fixed CSV and reshaping it, query the data directly and get exactly the columns and grouping you need.

For example, to build a monthly adoption slide for standalone, non-email presentations, aggregate presentation activity by month and user group. A user can belong to more than one group, so presentation_counts stores group membership as a comma-joined list. Split that list into one row per group before grouping:

WITH group_rows AS (
    SELECT
        DATE_TRUNC('month', p.created_on) AS month,
        ids.id AS user_group_id,
        names.name AS user_group,
        p.total_presentations
    FROM presentation_counts p
    CROSS JOIN unnest(string_to_array(p.user_group_ids, ',')) WITH ORDINALITY AS ids(id, pos)
    JOIN unnest(string_to_array(p.user_groups, ',')) WITH ORDINALITY AS names(name, pos)
        ON names.pos = ids.pos
    WHERE p.created_on >= CURRENT_DATE - INTERVAL '6 months'
        AND p.is_attachment = FALSE
        AND p.template_source_type <> 'email'
)
SELECT
    month,
    user_group_id,
    MAX(user_group) AS user_group,
    SUM(total_presentations) AS presentations
FROM group_rows
GROUP BY month, user_group_id
ORDER BY month, presentations DESC

As with any other piece of Dynamic Content, the result can drive a table, a chart, or an Insights summary.

Follow up with the recipients who engaged

Matik Analytics allows you to leverage your own Matik Mail performance analytics to drive personalization. For example, after a campaign, you may want to send a targeted follow-up only to people who showed interest. A Matik Analytics query returns the recipients who clicked in the last 30 days:

SELECT DISTINCT recipient_email
FROM email_events
WHERE event_type = 'click'
    AND occurred_at >= CURRENT_DATE - INTERVAL '30 days'
    AND flow_name = &:flow_name

Reference that Dynamic Content inside a query on your own contacts data to build the follow-up list:

SELECT email
FROM contacts
WHERE email IN (
    SELECT recipient_email
    FROM {{recently_engaged_recipients}}
)

This works in both directions. A Matik Analytics query can be nested inside another data source's query, and another data source's Dynamic Content can be nested inside a Matik Analytics query.

Selecting Matik Analytics as a Data Source

Matik Analytics can be used to power Dynamic Content or Inputs, just like any other data source. Once the beta is enabled for your enterprise, select Matik Analytics as the data source.

Querying Matik Analytics

Matik Analytics is organized into Analytics Categories. Each category contains related tables that you can query.

Category Included Data Tables Requires
Email analytics Email sends and engagement, current unsubscribed recipients, and unsubscribe-list freshness. email_sends, email_events, unsubscribed_recipients, suppression_status Matik Mail
Generation performance Timing for each step of the presentations your enterprise has generated. generation_times, generation_times_daily None
Presentation activity Current users, presentation generations and counts, scheduled and triggered flows, and Dynamic Content query timings, warnings, and errors. users, generations, presentation_counts, flows, flow_collection_status, dc_run_times, dc_errors None

The dropdown only lists categories your enterprise can use.

A single query reads from one category, so you cannot join a table in Email analytics to a table in Presentation activity. To combine them, build separate Dynamic Content for each and reference one inside the other.

Email Analytics

Available to enterprises with Matik Mail enabled.

email_sends - one row per recipient of a sent email, with delivery state and engagement totals.

Column Type Description
send_id integer Stable ID for this recipient's email send. Join to email_events.send_id.
sent_at timestamp UTC time when the send record was created.
recipient_email text Email address the message was sent to.
recipient_type text Whether the recipient was on the to, cc, or bcc line.
template_name text Name of the email template used.
template_id integer ID of the email template used. Stays the same if the template is renamed. Null if unavailable.
flow_name text Name of the scheduled or triggered flow that sent it, if any.
delivery_status text Delivery outcome for the message.
open_count integer Number of times this recipient opened the message.
click_count integer Number of link clicks by this recipient.
unsubscribed boolean Whether the recipient unsubscribed from this send.
spam_report boolean Whether the recipient reported the message as spam.
last_open_at timestamptz Most recent open in UTC time, or null if never opened.
last_click_at timestamptz Most recent click in UTC time, or null if never clicked.
inputs_json text Email input values as JSON object text. Cast to jsonb to query keys. {} means empty inputs; NULL means unavailable or invalid data.
generation_id integer ID of the generated email. All recipient rows for the same email share this ID; use COUNT(DISTINCT generation_id) to count generated emails. NULL when unavailable.
flow_id integer ID of the scheduled or triggered flow that sent the email. Stable across flow renames; NULL for manual flows or when unavailable.
run_id integer ID of the flow run that sent the email, including manual flows. NULL when unavailable.

These IDs identify the sending email, not an attached presentation. Join email_events.send_id = email_sends.send_id to trace engagement back to the email and flow within Email analytics.

email_events - one row per email engagement event, either an open or a click.

Column Type Description
send_id integer ID of the recipient's email send. Join to email_sends.send_id. Null if unavailable.
event_type text The kind of engagement, such as open or click.
occurred_at timestamptz UTC time when the event happened.
recipient_email text Email address that generated the event.
recipient_type text Whether the recipient was on the to, cc, or bcc line.
template_name text Name of the email template used.
template_id integer ID of the email template used. Stays the same if the template is renamed. Null if unavailable.
flow_name text Name of the scheduled or triggered flow that sent it, if any.
url text Clicked URL. Null for opens.
attachment_id integer Id of the clicked attachment. Null for opens and non-attachment clicks.
attachment_name text Name of the clicked attachment. Null for opens and non-attachment clicks.
attachment_template_id integer Template ID of the clicked attachment, which may differ from the sending email's template_id. Null when there is none.
attachment_template_name text Template of the clicked attachment. Null when there is none.
Querying email inputs

inputs_json contains the email presentation's input values, using the same input names and values as the Mail CSV. Values retain their JSON types, including arrays, numbers and booleans. Every to, cc and bcc recipient of the same generated email shares that input object; another email to the same recipient can have different inputs.

Cast the text to jsonb and use ->> to extract a value as text, or -> to keep its JSON type. For example, this query lists opened emails alongside an input value. Replace customer_name with one of your input names:

SELECT s.send_id, s.sent_at, s.recipient_email,
       s.inputs_json::jsonb ->> 'customer_name' AS customer_name
FROM email_sends s
WHERE s.inputs_json IS NOT NULL
  AND EXISTS (
    SELECT 1
    FROM email_events e
    WHERE e.send_id = s.send_id
      AND e.event_type = 'open'
  )
ORDER BY s.sent_at DESC
LIMIT 50;

Join sends and events on send_id. Several events can belong to one send, so use EXISTS or aggregate events before counting sends. The example above keeps one row per send, even after multiple opens.

Email inputs and event send IDs are available for ongoing reporting. Historical coverage is incomplete, so older rows may still have NULL inputs_json or NULL send_id. Some older sends have populated inputs, but do not assume complete historical coverage. For inputs, {} means a known empty object; SQL NULL means the input data is unavailable or invalid.

Currently unsubscribed recipients

unsubscribed_recipients - one row per currently unsubscribed email address for your enterprise, refreshed hourly from SendGrid. This is a current list, not a history of unsubscribe events or a list of Matik account users.

The list includes recipients in your enterprise's configured unsubscribe group. Global unsubscribes are included only when your enterprise also has a send marked as globally unsubscribed, matching the existing unsubscribe CSV rule. It does not include every globally unsubscribed address your enterprise has ever mailed. Hard bounces and blocks are not included.

Column Type Description
email text Recipient email, trimmed and lowercased. Plus tags and dots are preserved.
suppression_scope text group, global, or both. An address in both lists appears once.
group_unsubscribed_at timestamptz UTC group unsubscribe time reported by SendGrid, or NULL if unavailable.
global_unsubscribed_at timestamptz UTC global unsubscribe time reported by SendGrid, or NULL if unavailable.
observed_at timestamptz UTC time when collection of this list completed.
is_stale boolean True when the list was collected more than three hours ago.

suppression_status - one row describing the latest successful collection and latest attempt for your current unsubscribe configuration, even when the recipient list is empty.

Column Type Description
observed_at timestamptz UTC completion time of the latest successful collection for the current configuration, or NULL if none is available.
active_count integer Number of recipients in that collection, including zero. NULL when no successful collection is available.
latest_attempt_at timestamptz UTC completion time of the latest collection attempt for the current configuration, or NULL if none is recorded.
latest_attempt_status text complete or failed; NULL if no attempt is recorded. A failed refresh preserves the last successful list.
is_stale boolean True when no successful collection is available for the current configuration or it is more than three hours old.
status text ready, stale, or not_ready. Check this before interpreting an empty recipient list.
View current unsubscribes and check freshness

In Email analytics, run this query to list currently unsubscribed recipients:

SELECT email, suppression_scope,
       group_unsubscribed_at, global_unsubscribed_at,
       observed_at, is_stale
FROM unsubscribed_recipients
ORDER BY email
LIMIT 50;

Run this separately to check whether an empty result is a current empty list or collection is not ready:

SELECT observed_at, active_count, latest_attempt_at,
       latest_attempt_status, is_stale, status
FROM suppression_status;
  • ready with active_count = 0 means collection succeeded and found no recipients under these rules.
  • not_ready means no successful collection is available for the current configuration. An empty recipient result does not establish that nobody is unsubscribed.
  • stale means the last successful collection is more than three hours old. Check latest_attempt_status for refresh failures; the last successful list remains available while the configuration matches.

To join to sends or events, match unsubscribed_recipients.email to LOWER(TRIM(email_sends.recipient_email)) or LOWER(TRIM(email_events.recipient_email)). This identifies recipients who are unsubscribed now, not whether they were unsubscribed when an older send or event occurred. The per-send email_sends.unsubscribed flag is not a substitute for the current list.

After an unsubscribe group or integration configuration change, the previous configuration's list is hidden until a successful collection for the new configuration is available. The table does not identify which email or flow caused an unsubscribe.

Generation Performance

Generation Performance contains processing-time measurements for your enterprise's presentation generations. Use it to track changes in typical and slower-run timings over time, or compare timings for different processing stages.

generation_times contains individual timing events for each measured stage. One generation can produce several events. generation_times_daily summarizes those events by day and stage, with a run count, median (p50), and 95th percentile (p95). The p95 is the time at or below which 95% of the recorded timings fall.

generation_times includes presentation_id and template_id, so you can identify the generated presentation and compare processing times by template. generation_times_daily remains a summary by day and stage, without presentation or template IDs.

generation_times - one row per pipeline step of a presentation generation, with its elapsed time.

Column Type Description
occurred_on date Day the generation ran.
event_name text Pipeline step that reported this timing.
elapsed_ms bigint How long the step took, in milliseconds.
presentation_id integer ID of the generated presentation. NULL when unavailable.
template_id integer ID of the template used to generate the presentation. Stays the same if the template is renamed. NULL when unavailable.

generation_times_daily - daily rollup of generation timings per pipeline step: run count, median, and 95th percentile.

Column Type Description
occurred_on date Day the runs happened.
event_name text Pipeline step that reported these timings.
runs integer Number of times the step ran that day.
p50_ms numeric Median elapsed milliseconds across the day's runs.
p95_ms numeric 95th percentile elapsed milliseconds across the day's runs.
Compare processing time by template

Select the Generation performance category and run this query to compare average presentation handler time by template, in seconds:

SELECT template_id,
       COUNT(*) AS recorded_timings,
       ROUND(AVG(elapsed_ms) / 1000.0, 2) AS average_seconds
FROM generation_times
WHERE template_id IS NOT NULL
  AND event_name = 'lambda_presentation_handler'
GROUP BY template_id
ORDER BY average_seconds DESC;
Compare presentation processing time by day

Select the Generation performance category and filter to lambda_presentation_handler to summarize the broader presentation handler timing. This query returns the number of recorded handler timings and their average, median, and p95 in seconds for the last 30 complete days:

SELECT
    occurred_on,
    COUNT(*) AS recorded_timings,
    AVG(elapsed_ms) / 1000.0 AS average_seconds,
    PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY elapsed_ms) / 1000.0 AS median_seconds,
    PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY elapsed_ms) / 1000.0 AS p95_seconds
FROM generation_times
WHERE event_name = 'lambda_presentation_handler'
    AND occurred_on >= CURRENT_DATE - INTERVAL '30 days'
    AND occurred_on < CURRENT_DATE
GROUP BY occurred_on
ORDER BY occurred_on;

The handler timing includes work measured by other stages. Do not add the stage timings together: that would double-count overlapping work. This is processing time inside the handler, not the full wait from clicking Generate to receiving the finished presentation.

For daily median and p95 trends without an average, query generation_times_daily with the same event filter. To calculate an average or percentiles across a longer period, aggregate the individual events in generation_times; averaging daily medians or p95s does not produce the median or p95 for the full period.

Presentation Activity

Scheduled and triggered flows

flows - one row per current scheduled or triggered flow, including flows that have never run and unfinished triggered flows. Manual flows and auto-refresh schedules are excluded.

This is current configuration, not a history of flow runs. Data refreshes hourly; a failed collection preserves the last successful snapshot. The latest run is the highest-ID bulk run shown on the Flows page.

Column Type Description
flow_id integer Stable flow ID; stays the same after a rename.
flow_name text Current flow name.
flow_type text scheduled or triggered.
status text active, inactive, or completed at snapshot time.
user_id integer Creator ID; join to users.user_id.
template_id integer Template ID, or NULL for an unfinished trigger.
template_name text Current template name, if available.
created_on timestamp UTC flow creation time.
updated_on timestamp UTC time the flow definition was last edited.
effective_start timestamp UTC schedule start.
effective_end timestamp UTC schedule end; never-ending flows use a far-future date.
cadence text daily, weekly, or monthly; NULL if not configured.
cadence_interval integer Every N days, weeks, or months.
cadence_day text Readable UTC schedule day, such as Monday, last day, or first Monday.
end_type text on a date, after N runs, or never.
end_after integer Configured occurrence limit, not the number of completed runs.
next_run_at timestamp Next scheduled UTC occurrence at snapshot time; NULL for inactive, completed, or unscheduled flows. For triggered flows, this is the next check, which may not generate anything.
last_run_at timestamp Completion time of the latest bulk run, falling back to its creation time; NULL before its first run.
last_run_status text Status of that latest bulk run, including an in-progress run; NULL before its first run.
inputs_json text Saved input values as JSON object text. Bulk values form a list. {} means known empty; SQL NULL means unavailable.
bulk_input_name text Name of the bulk input, if configured.
observed_at timestamp UTC snapshot time; use this to check how recently the data was collected.

inputs_json contains saved setup values, not the current options returned when a flow re-fetches its inputs. Cast to jsonb to query individual keys. CSV rows with differing non-bulk values return SQL NULL because one object cannot preserve their row pairings.

flow_id identifies the same flow as email_sends.flow_id, but Email analytics is a separate category. Use nested Dynamic Content to combine those results.

Flow snapshot freshness

flow_collection_status - one row describing the latest successful flow snapshot and latest attempt, even when the inventory is empty.

Column Type Description
freshness text ready, stale (last success more than three hours ago), or not_collected.
last_success_at timestamp UTC start of the last successful snapshot; NULL before collection.
latest_attempt_at timestamp UTC start of the latest attempt, if any.
latest_attempt_status text running, complete, or failed; NULL before collection.
duration_seconds numeric Duration of the latest finished attempt; NULL while running.
row_count integer Flow count in the last successful snapshot; zero means a known empty inventory.
payload_bytes bigint UTF-8 saved-input JSON bytes in the last successful snapshot.

Check freshness before interpreting an empty flows result. ready with row_count = 0 means collection succeeded and found no flows. not_collected means no successful snapshot exists. A failed attempt can coexist with a previous successful snapshot.

See upcoming flows

In Presentation activity, list active flows with their next scheduled run or trigger check:

SELECT flow_id, flow_name, flow_type, template_name,
       next_run_at, last_run_status
FROM flows
WHERE status = 'active'
ORDER BY next_run_at ASC NULLS LAST
LIMIT 50;

Run this separately to check freshness:

SELECT freshness, last_success_at, latest_attempt_status, row_count
FROM flow_collection_status;

presentation_counts - daily presentation generation counts. One row per UTC day, user, template, and whether the row counts rendered email attachments.

Column Type Description
created_on date UTC day the presentations were created. The current partial day is dropped, so days are only ever complete.
user_id integer Id of the user who created the presentations.
user_name text Name of the creating user, as of the row's last sync.
user_email text Email of the creating user.
user_group_ids text Comma-joined ids of the groups the user belongs to. The stable key for group filters.
user_groups text Comma-joined names of the groups the user belongs to, in the same order as user_group_ids. Display text, as of the row's last sync.
is_producer boolean Whether the user holds a non-consumer role. Matched by role name, so custom roles count.
template_id integer Id of the template the presentations were generated from.
template_name text Name of the template, as of the row's last sync.
template_source_type text Kind of template: powerpoint, google_slides, email, and so on. email marks email shell rows.
generation_source text

Where the generation was started:

  • web_app: The Matik web app
  • sfdc_app: The Matik App for Salesforce
  • mcp: An AI tool connected to Matik, such as Claude or ChatGPT
  • external_api: The Matik API
  • assistant: Matik's AI in-app
  • scheduled_flow: A flow that runs at a scheduled time
  • triggered_flow: A flow that runs when your data changes
  • NULL: Not recorded
is_attachment boolean Whether the row counts presentations rendered as email attachments.
total_presentations bigint Completed presentations created that day. Test runs and Mail Outbox previews are excluded, and deleted presentations still count.
total_failed bigint Presentations that failed that day through an error, a failed condition, or a timeout.

users - one row per current Matik account user, including inactive users and users with no presentation activity. Deleted accounts are hidden. Names, roles, groups, and status reflect the latest complete roster snapshot.

Column Type Description
user_id integer Stable user ID for joining to presentation_counts.user_id or generations.user_id.
email text Current account email address.
name text Current user name.
title text Current user title, if set.
provisioned_at timestamp UTC time when the account was created.
status text Current account status: active or inactive.
role_names text Current role names, comma-separated in role-ID order; empty if none.
group_ids text Current group IDs, comma-separated; use these for exact membership.
group_names text Current group names for display, joined with comma-space. A group name itself can contain a comma.
observed_at timestamp UTC time of the latest complete roster snapshot.

generations - one row per presentation generation. Includes today's generations, generations still in progress, failed generations, and deleted presentations.

Column Type Description
generation_id integer ID of the presentation. Stable, so you can use it to join to other data.
created_at timestamp UTC time the generation started (not when it finished).
user_id integer ID of the user who created it. Null if unavailable.
template_id integer ID of the template it was generated from. Null if unavailable.
status text done means it succeeded. error, failed condition, and timeout mean it failed. Other values mean it is still in progress.
generation_source text Where the generation was started. Same values as in presentation_counts.
presentation_type text Kind of presentation. email marks the email itself, as opposed to its attachments.
is_deleted boolean Whether the presentation was deleted.
bulk_presentation_id integer ID of the bulk generation run it belongs to, if any.
is_attachment boolean Whether the row is a presentation rendered as an email attachment.
inputs_json text Presentation inputs as JSON object text. Keys are input names and values are raw values (IDs rather than display labels). {} means known empty; SQL NULL means unavailable or invalid source data. Reflects the latest synced presentation inputs.
Querying presentation inputs

In Presentation activity, generations.inputs_json contains each presentation's input values, including bulk children, scheduled generations and rendered email attachments. Keys are input names; values retain their JSON types, including arrays, numbers and booleans. Values are raw values such as account IDs, rather than display labels. Renaming an input can change its key.

Cast the text to jsonb and use ->> to extract a value as text, or -> to retain its JSON type. For example, count unique accounts by month and template. Replace account_id with one of your actual input names:

SELECT date_trunc('month', created_at) AS month,
       template_id,
       COUNT(DISTINCT inputs_json::jsonb->>'account_id') AS unique_accounts
FROM generations
WHERE status = 'done'
  AND presentation_type <> 'email'
GROUP BY 1, 2
ORDER BY 1, 2;

This query excludes email shells and retains successfully rendered attachments. SQL NULL values and missing keys do not contribute to the distinct count.

Inputs reflect the latest synced presentation state, including regeneration on an existing presentation, rather than a separate historical snapshot for each attempt. Generation history starts October 9, 2026 at 21:36 UTC; earlier presentations are not included. {} means known empty, while SQL NULL means unavailable or invalid source data. Some source input data may be missing or invalid, so a presentation can have NULL inputs even when its generation is present. Recent changes appear after the normal analytics sync delay.

dc_run_times - one row per data source query execution during a production presentation generation, including each loop iteration. The duration measures the data source call, not query preparation, formatting, rendering, or total generation time.

Column Type Description
execution_id text Stable ID of this query execution.
presentation_id integer Presentation generation ID; join to generations.generation_id.
template_id integer Template ID at generation time.
user_id integer ID of the user who created the presentation.
generation_attempt_id text Generation attempt ID, when available.
dc_id integer Stable Dynamic Content ID, including after a rename.
dc_version_id integer Version of the Dynamic Content that ran, if available.
dc_name text Dynamic Content name captured when the query ran.
dc_type text Dynamic Content type captured when the query ran.
data_source_type text Data source type used by this query.
loop_index integer Zero-based loop iteration; null when the content was not looped.
started_at timestamp UTC time when the data source query began.
finished_at timestamp UTC time when the data source query finished.
elapsed_ms numeric Time spent in the data source query, in milliseconds.
row_count integer Rows returned, excluding the header. Null for non-tabular results or when rows and columns were swapped.
outcome text Query result: success, error, or timeout.
succeeded boolean Whether the data source query succeeded.
is_bulk boolean Whether the query ran as part of a bulk generation.

Query timings are available from October 15, 2026 onward. Earlier timings were not backfilled. Cached content and content that does not make a separate data source call have no row. A timeout stopped by the generation time limit may include only the elapsed time observed before that limit.

Find slow Dynamic Content queries

This query ranks Dynamic Content by the 95th percentile of successful query durations in the last 30 days. It requires at least 10 queries per ID so a single slow query does not dominate the list:

SELECT dc_id, MAX(dc_name) AS dc_name,
       COUNT(*) AS queries,
       PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY elapsed_ms) AS p95_ms
FROM dc_run_times
WHERE started_at >= CURRENT_DATE - INTERVAL '30 days'
    AND outcome = 'success'
    AND dc_id IS NOT NULL
GROUP BY dc_id
HAVING COUNT(*) >= 10
ORDER BY p95_ms DESC
LIMIT 50;

dc_errors - one row per Dynamic Content warning or error during a production presentation generation. A generation can have several issues, and a warning does not mean the whole generation failed. The message is a short summary; the full details remain in the generation log.

Column Type Description
issue_id text Stable ID of this diagnostic event.
occurred_on timestamp UTC time when the warning or error occurred.
presentation_id integer Presentation generation ID; join to generations.generation_id when that generation is available.
generation_log_url text Link to the full generation log, subject to normal access controls.
template_id integer Stable template ID, if available.
template_name text Template name captured for this row, if available.
user_id integer ID of the user who generated the presentation, if available.
user_email text Generating user email captured for this row, if available.
dc_id integer Stable Dynamic Content ID. Null if the issue could not be attributed to an ID.
dc_name text Dynamic Content name recorded in the generation log.
dc_type text Dynamic Content type recorded by the generation, when available.
data_source_type text Data source type recorded by the generation, when available.
attribution_status text How the Dynamic Content ID was found: explicit_id, run_local_id, or unresolved.
issue_level text Log severity: error or warning. A query failure can appear as a warning.
error_category text Diagnostic type, useful for grouping similar issues.
message text Short summary. Use generation_log_url for full details if you have access.
is_bulk boolean Whether the issue happened during a bulk generation.

Recent diagnostics are available, and some older events have been recovered from generation logs. Historical coverage may be incomplete. Use dc_id to group across name changes. A null dc_id means the issue could not be reliably attributed to a Dynamic Content ID.

Find Dynamic Content with warnings or errors

Count both issue events and affected generations. One generation can have several issues:

SELECT dc_id, MAX(dc_name) AS dc_name,
       COUNT(*) AS issue_events,
       COUNT(DISTINCT presentation_id) AS affected_generations
FROM dc_errors
WHERE occurred_on >= CURRENT_DATE - INTERVAL '30 days'
    AND dc_id IS NOT NULL
GROUP BY dc_id
ORDER BY affected_generations DESC, issue_events DESC
LIMIT 50;

Things to note:

  • generation_source data started getting tracked in September 2026, so generations from before then may show NULL.
  • generations starts on October 9, 2026 at 21:36 UTC. Earlier presentations are not included. Use presentation_counts for history before that date.

 

  • An email send creates a shell row (template_source_type = 'email') plus a row for each rendered attachment. Filter on is_attachment or template_source_type so sends are not counted twice.
  • generations and presentation_counts count different sets of rows. presentation_counts leaves out the current day, in-progress generations, and rows with no creator or template. Use the same filters on both before comparing totals.

Writing Queries

Matik Analytics accepts one read-only PostgreSQL query at a time. Start your query with SELECT or WITH ... SELECT, and use only the tables listed for the selected Analytics Category.

Matik Analytics supports referencing &:inputs and {{nested_dc}}, just like any other SQL query in Matik. These references can be used anywhere their resolved values are valid, including using nested table Dynamic Content in a FROM or JOIN clause. Matik Analytics Dynamic Content can also be nested in another SQL data source that supports table Dynamic Content.

Supported SQL

  • Joins between tables in the same Analytics Category
  • Subqueries and common table expressions (CTEs), including recursive CTEs
  • Aggregates and window functions
  • Set operations such as UNION, INTERSECT, and EXCEPT
  • Standard PostgreSQL expressions and functions, except for the restricted operations described below

SQL that is not supported

  • More than one SQL statement in a query. A single trailing semicolon is allowed.
  • Statements that write data or modify the database, including INSERT, UPDATE, DELETE, MERGE, CREATE, DROP, ALTER, and TRUNCATE
  • Database administration or session commands, including GRANT, REVOKE, COPY, SET, EXPLAIN, SHOW, and VACUUM
  • Locking or table-creation forms such as FOR UPDATE and SELECT INTO
  • Schema-qualified table names, such as public.email_sends
  • System catalogs, raw Matik application tables, or tables from another Analytics Category
  • A CTE that uses the same name as one of the category's built-in tables
  • Functions that inspect or modify the database server or session, access server files, open external database connections, or pause or control database processes

Matik validates each query before it runs and displays an error when the query uses unsupported SQL.

You can also ask the Matik AI Agent to write or update an analytics query for you, rather than authoring the SQL yourself.

Limitations

  • Data freshness varies by table. Flow inventory refreshes hourly; check flow_collection_status for freshness and the latest collection attempt. Current unsubscribe membership refreshes hourly; check suppression_status for collection status and freshness. Generation performance timing data refreshes daily. Most analytics data syncs throughout the day and is typically less than two hours behind the activity it describes. dc_run_times and dc_errors are recorded as generation telemetry is received; some diagnostics recovered from generation logs may appear later. In presentation_counts, the current partial day is always excluded, so the most recent day you can query is the last complete UTC day. generations includes the current day, up to the latest sync. users.observed_at shows when the current roster was last refreshed, not when an individual account was edited.
  • One statement per query. A second statement after a semicolon is rejected.
  • One category per query. Tables from different categories cannot be joined in a single query.
  • Query text is limited to 1 MB.
  • Query timeout. Matik Analytics queries that run longer than 60 seconds are stopped.
  • Saved results. A saved query result holds up to 50,000 rows.
  • Result size. A saved query result can hold up to 16 MB of data, and a test run in the query editor up to 256 KB. A query that returns more fails with "This query returned more data than Matik Analytics allows. Select fewer rows or narrower columns."

Best Practices

  • Filter out attachment rows when counting unique sends. In presentation_counts and generations, an email send produces both an email row and attachment rows. Without a filter on is_attachment together with template_source_type (in presentation_counts) or presentation_type (in generations), the query will count both the email and any attachments.
  • Filter and join on template IDs, not names. Template names can change. Some tables store the name from the latest sync, while dc_errors captures a name for each issue. template_id stays the same, so a query that filters on it keeps working after a rename.
  • Use generations when you need individual presentations. It lists each presentation's ID and exact time, which you need for per-generation reporting or for joining to other data. For daily trends, presentation_counts returns far fewer rows.
  • Split group membership before you group by it. user_group_ids and user_groups are comma-joined lists, so grouping on either column groups by the whole combination of a user's groups rather than by one group at a time. Split them into one row per group first, as in the example above. Note that this drops users who belong to no group; switch the CROSS JOIN to a LEFT JOIN ... ON true if you need those rows.
  • Group by user_group_ids, display user_groups. Group names change and are stored as of the last sync. The ids are stable, so group on those and use the names for display.
  • Keep users with no presentation activity. Start from users and LEFT JOIN an activity summary on user_id. Use status = 'done' when you mean successful generations. For standalone, non-email presentations, also exclude attachments and email shells.
  • Use generation_times_daily for trends. It is already rolled up per day and step, so it returns far fewer rows than generation_times and stays well clear of the saved-result cap.
  • Use nested Dynamic Content to query across Analytics Categories. Since a query reads from a single category, combining email and presentation data means referencing one Dynamic Content inside another.

For example, this query keeps users who have no successful standalone generations in the available generations history. Their last_success_at is null:

WITH latest AS (
    SELECT user_id, MAX(created_at) AS last_success_at
    FROM generations
    WHERE status = 'done'
        AND is_attachment = false
        AND presentation_type <> 'email'
    GROUP BY user_id
)
SELECT u.user_id, u.name, u.status, l.last_success_at
FROM users u
LEFT JOIN latest l ON l.user_id = u.user_id
ORDER BY u.user_id
LIMIT 50

Was this article helpful?

Have more questions? Submit a request

Related articles

  • Template Recommendations [Beta]
  • Saving Matik Generated Content
  • [BETA] Version History and Drafts for Matik Mail Templates
  • Using SSO's to sign in to Matik (Okta, Onelogin, Azure, Google)
  • Using Looker as a Dynamic Content Data Source

Articles in this section

  • [BETA] Using Omni with Dynamic Content
  • Using Decision Dynamic Content [Beta]
  • Using Matik Analytics as a Data Source
  • [BETA] Draft Versions for Dynamic Content and Inputs
  • Using Asana in Dynamic Content
  • Dynamic Content Overview
  • Version History for Dynamic Content
  • AI Descriptions
  • Using Automated Insights Dynamic Content
  • Working with Images in Dynamic Content
  • See all articles

Comments

0 comments

Please sign in to leave a comment.

Personalize data-driven content in minutes

Product

  • How it Works
  • Integrations
  • AI Features
  • Security

Solutions

  • Sales
  • Customer Success
  • Ops & Strategy
  • Data

Resources

  • Blog
  • Templates
  • AI + CS Resource Hub
  • Case Studies
  • Help Center

Company

  • About Us
  • Careers
  • Terms of Service
  • Privacy Policy

© 2024 Matik, Inc.