Beta Feature: Matik Analytics is currently in beta. Contact your Technical Account Manager if you are interested in access.
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 DESCAs 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_nameReference 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 | Sends and engagement for emails your enterprise has sent through Matik. |
email_sends, email_events
|
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, and Dynamic Content query timings, warnings, and errors. |
users, generations, presentation_counts, 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, including on some older rows. |
flow_name |
text | Name of the scheduled 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, including on some older sends. |
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, including on some older rows. |
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, including on some older rows. |
flow_name |
text | Name of the scheduled 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 uses the same input names and values as the Mail CSV. 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.
Input and event send-ID coverage was enabled on October 14, 2026. Historical data was not fully backfilled, so some older rows still have NULL inputs_json or NULL send_id.
Generation Performance
How long each step of a presentation generation takes. Use generation_times_daily for reporting and generation_times when you need individual runs.
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. |
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. |
Presentation Activity
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:
|
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. |
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. Older recovered events may use a later name. |
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. Older recovered events may use a later email. |
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:
- An email send creates a shell row (
template_source_type = 'email') plus a row for each rendered attachment. Filter onis_attachmentortemplate_source_typeso sends are not counted twice. -
generation_sourcedata started getting tracked in September 2026, so generations from before then may showNULL. -
generationsstarts on October 9, 2026 at 21:36 UTC. Earlier presentations are not included. Usepresentation_countsfor history before that date. -
generationsandpresentation_countscount different sets of rows.presentation_countsleaves 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, andEXCEPT - 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, andTRUNCATE - Database administration or session commands, including
GRANT,REVOKE,COPY,SET,EXPLAIN,SHOW, andVACUUM - Locking or table-creation forms such as
FOR UPDATEandSELECT 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. Most analytics data syncs throughout the day and is typically less than two hours behind the activity it describes.
dc_run_timesanddc_errorsare recorded as generation telemetry is received; some diagnostics recovered from generation logs may appear later. Inpresentation_counts, the current partial day is always excluded, so the most recent day you can query is the last complete UTC day.generationsincludes the current day, up to the latest sync.users.observed_atshows 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_countsandgenerations, an email send produces both an email row and attachment rows. Without a filter onis_attachmenttogether withtemplate_source_type(inpresentation_counts) orpresentation_type(ingenerations), 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_errorscaptures a name for each issue.template_idstays the same, so a query that filters on it keeps working after a rename. -
Use
generationswhen 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_countsreturns far fewer rows. -
Split group membership before you group by it.
user_group_idsanduser_groupsare 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 theCROSS JOINto aLEFT JOIN ... ON trueif you need those rows. -
Group by
user_group_ids, displayuser_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
usersand LEFT JOIN an activity summary onuser_id. Usestatus = 'done'when you mean successful generations. For standalone, non-email presentations, also exclude attachments and email shells. -
Use
generation_times_dailyfor trends. It is already rolled up per day and step, so it returns far fewer rows thangeneration_timesand 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
Comments
0 comments
Please sign in to leave a comment.