There are two places to get figures from:
- Analytic Center has prepared reports: sales, leads, tickets, website traffic and more. Its numbers have fixed definitions, so use them whenever one of them answers the question.
- Dashboard Builder (the screen is titled Data Studio) holds the person's own queries, charts and dashboards. Use it for anything the reports don't show, or when the person wants a dashboard to keep.
Other figures have their own pages:
- A broadcast's opens and clicks: Broadcast report.
- What an AI employee did and spent: Review what an AI employee did.
- The business's own Appmint usage and charges: Your Appmint plan, credit, usage and services.
This skill only reads data. It sends nothing to anyone. Saving a chart or a dashboard only adds a record in the business's own Studio.
Ask first
- What is the question, in their words? Pin down three things:
- the figure (revenue, orders, leads, visitors);
- what to split it by (status, product, month, source);
- the period.
- Which period? Say which dates you use. The Analytic Center defaults to the last 30 days. A new dashboard defaults to All time.
- Just the answer, or something to keep? Answer in words unless they ask for a chart or a dashboard. Save nothing they didn't ask for.
- Add to which dashboard? When adding a chart, ask which existing dashboard it goes on, by name.
- Which site? Ask for website and blog figures when the business has more than one site.
Steps
1. Analytic Center: is it already reported?
Each dashboard is one call_appengine GET with startDate and endDate (YYYY-MM-DD) in query. On screen it is Analytic Center (/app/analytics), one tab per dashboard (appengine analytics/analytics.controller.ts).
| Dashboard | GET | Its overview | Also in the answer |
|---|---|---|---|
| Website | /analytics/website (+ siteName) | totalVisitors, pageViews, sessions, bounceRate, avgSessionDuration, conversionRate, botRate… | traffic, topPages, entryPages, exitPages, trafficSources, topReferrers, utmAttribution, geographicData, deviceBreakdown, formAnalytics, newVsReturning |
| Blog | /analytics/blog (+ siteName) | totalPosts, postsPublished, totalViews, uniqueReaders, totalComments | topPosts, engagementTrends, categoryPerformance, authorPerformance; only for a site whose blog is on |
| Storefront | /analytics/storefront | totalRevenue, totalOrders, paidOrders, avgOrderValue, returnCustomers, pendingOrders | productPerformance, channelPerformance, salesTrends, revenueTrends, ordersByStatus, returns, customerSegments |
| Leads | /analytics/leads (+ pipelineId, onlyPipeline, tz) | totalLeads, qualifiedLeads, conversionRate, avgDealSize, pipelineValue | leadSources, funnelData, pipelineFunnels, leadTrends, campaignPerformance |
| Tickets | /analytics/tickets | totalTickets, openTickets, resolvedTickets, avgResolutionTime, customerSatisfaction | ticketsByPriority, ticketsByCategory, agentPerformance |
| Workflow | /analytics/workflow | totalTasks, completedTasks, openTasks, overdueTasks | tasksByStatus, assigneePerformance, workloadDistribution |
| Automation | /analytics/automation | activeWorkflows, totalExecutions, successRate, failedExecutions | workflowPerformance, errorAnalysis, executionTrends |
| User accounts | /analytics/users | totalUsers, newUsers, activeUsers, payingUsers, churnRate | userGrowth, subscriptionTiers |
/analytics/email | totalSent, delivered, opened, clicked, bounced, unsubscribed | rates, campaignMetrics, dailySends | |
| Live View | /analytics/live-view (minutes, default 15) | visitors on the site now | each visitor's journey: /analytics/live-view/<deviceId>/journey |
- The period before. Every dashboard except Live View also answers with a
periodand acomparison: the same length of time just before it. Eachoverviewfigure carries itspreviousvalue,changePercent,direction(up,downorflat) andsentiment(goodorbad; for bounce rate, open tickets, overdue tasks, failures and bounced email, down is good). Say both: "$4,210 revenue, up 12% on the 30 days before." - Dates. Without dates a report covers the last 30 days up to now. A date-only
endDateincludes that whole day in UTC. Leads also takestz, the viewer's time zone. The Analytic Center screen remembers the last range picked, per browser. - Revenue is money taken. Storefront revenue counts orders whose
data.statusispaid,paid-partial,overpaid,processing,shipped,in_transit,out_for_delivery,deliveredorcompleted. An unpaid, cancelled or refunded order is counted as an order but not as revenue. An order's amount isdata.amount, ordata.totalwhen there is no amount (analytics/analytics-query.ts92–98). - Website figures come from the platform's own visit tracking on the site, not from Google Analytics. For Google Analytics and the Facebook pixel, see Google Analytics and the Facebook pixel.
- Quick counts. Each Studio section's home screen shows a few counts.
GET /studio-overview/<section>returns them, forhome,account,config,database,build-studio,dam,crm,store,events,community,finance,logisticsandai-automation. For example,storegives orders, orders to fulfil, outstanding and overdue invoices, active subscriptions and revenue over the last 30 days. Tested.
Hands on: the figures, the exact dates, and the change on the period before. Check: open Analytic Center on the same dashboard and dates. The numbers match the answer. If a report answers the question, stop here.
2. Dashboard Builder: write the query and test it
Anything else is a MongoDB aggregation pipeline over one collection. Run it with aggregate, passing datatype and pipeline. It must return real rows before anything is saved.
- Field names. Read them with
get_schemaon the collection. A record's own fields sit underdata.($data.status,$data.amount).createdate,modifydate,stateandsksit at the top of the record. In Data Studio the Schema panel beside the query shows the same thing. - End with
$project, so the row holds plain top-level fields (month,count, not_id.month). Charts only see top-level fields. - Add
$sort, and a$limit(500 is plenty) on anything that could grow. - Use
$ifNullso missing values land in one bucket ("unknown") and don't spread across many. - Use only collections the person can read. The tool refuses one they can't see, such as finance or payroll. When that happens, tell them they don't have access. Don't look for the figure elsewhere.
Orders by status:
[
{ "$group": { "_id": { "$ifNull": ["$data.status", "unknown"] }, "count": { "$sum": 1 } } },
{ "$project": { "_id": 0, "status": "$_id", "count": 1 } },
{ "$sort": { "count": -1 } }
]Revenue by month for a date range, counting only money taken as the Analytic Center does:
[
{ "$match": { "$expr": { "$and": [
{ "$gte": ["$createdate", { "$toDate": "2026-07-01" }] },
{ "$lte": ["$createdate", { "$toDate": "2026-09-30T23:59:59.999Z" }] } ] } },
"data.status": { "$in": ["paid", "paid-partial", "overpaid", "processing", "shipped", "in_transit", "out_for_delivery", "delivered", "completed"] } } },
{ "$group": { "_id": { "$dateToString": { "format": "%Y-%m", "date": "$createdate" } },
"revenue": { "$sum": { "$convert": { "input": { "$ifNull": ["$data.amount", "$data.total"] }, "to": "double", "onError": 0, "onNull": 0 } } } } },
{ "$project": { "_id": 0, "month": "$_id", "revenue": 1 } },
{ "$sort": { "month": 1 } }
]createdate and modifydate are stored as dates, not text. A match such as { "createdate": { "$gte": "2026-09-01" } } compares a date with text and matches nothing. Tested: 0 rows, where the $expr and $toDate form above found all 10. Always compare those two fields through $toDate. A direct comparison works only on a field stored as ISO text.
Hands on: the pipeline that returned rows, the collection, and the names of the result fields. Check: the rows look right. Read two or three back to the person. If the pipeline errors or returns nothing, fix it and run it again. Never save a query that hasn't returned rows.
3. Dashboard Builder: save the chart, table or number (only when asked)
A saved chart or table is a dataviz_item. Create it with create_record:
{
"datatype": "dataviz_item",
"data": {
"name": "orders-by-status",
"title": "Orders by status",
"description": "Every order, by its status",
"type": "chart",
"content": {
"collection": "sf_order",
"mongoQuery": "<the pipeline as a JSON string>",
"viewConfig": {
"chartType": "pie",
"fieldMapping": { "categoryField": "status", "valueFields": ["count"], "seriesField": "" }
}
}
}
}| Field | What it is |
|---|---|
name | Required and unique: 3 to 80 letters, digits, - or _. Check with find_records that it isn't taken. |
type | chart draws a chart, table draws the rows as a table, text shows content.text. Studio's Save writes chart, unless the chart type picked is Table. |
content.collection | The collection the pipeline runs on. |
content.mongoQuery | The pipeline as a JSON string, not an array. |
viewConfig.chartType | bar, line, area, pie, doughnut, scatter, radar, gauge, funnel, heatmap, treemap, sunburst, boxplot or sankey |
viewConfig.fieldMapping | categoryField: the x-axis, or the slice name. valueFields: the numbers. seriesField: one series per value (optional). stack: true stacks bars or lines. When the mapping is empty, the first text field and up to three number fields are picked for you. |
viewConfig.chartOverride | Optional chart options as a JSON string, merged over the chart (colours, legend). |
How each kind of chart reads its fields:
- Pie, doughnut, funnel, treemap, sunburst: one
categoryFieldand one value. - Heatmap:
categoryFieldacross,seriesFielddown, one value. - Sankey:
categoryFieldis the source andseriesFieldthe target. - Gauge: uses only the first row.
A number card, for a big figure such as total revenue, is a table item whose pipeline returns one row, for example [{ "$count": "customers" }]. On the dashboard it gets "valuesOnly": "customers" (step 4), which shows just that figure, large. Tested: it showed 10. Several figures can be named, separated by commas.
Date range. A pipeline can use {{startDate}} and {{endDate}} in place of fixed dates, and a dashboard fills them from its date range. They are swapped in as text: the start as 2026-09-01 and the end as 2026-09-30T23:59:59.999Z (websitemint data-viz/data-viz-store.ts 539). So wrap them in $toDate against createdate, as in step 2: { "$gte": ["$createdate", { "$toDate": "{{startDate}}" }] }.
aggregate doesn't fill the placeholders. Test with real dates (step 2), then swap in {{startDate}} and {{endDate}} when you save. Tested on a dashboard set to the last 30 days: $toDate("{{startDate}}") found 10 customers, while a plain text comparison found none.
Hands on: each saved item's sk, which the dashboard needs, and its name.
Check: find_app for dashboard-builder, then navigate with itemId: "<sk or name>". The query editor opens the item and runs it straight away, and its results and chart are what you tested.
4. Dashboard Builder: lay out the dashboard (only when asked)
A dashboard is a dataviz record. It holds sections, stacked top to bottom. Each section holds columns, side by side and of equal width. Each column is one card. Create it with create_record:
{
"datatype": "dataviz",
"data": {
"name": "sales-overview",
"title": "Sales overview",
"filters": { "dateRange": { "preset": "last-30" } },
"sections": [
{ "id": "section-kpis", "columns": [
{ "id": "col-revenue", "type": "LayoutCard", "title": "Revenue", "items": ["<number item sk>"], "valuesOnly": "revenue", "style": { "height": "160px" } },
{ "id": "col-orders", "type": "LayoutCard", "title": "Orders", "items": ["<number item sk>"], "valuesOnly": "orders", "style": { "height": "160px" } }
] },
{ "id": "section-trends", "columns": [
{ "id": "col-by-month", "type": "LayoutCard", "items": ["<chart item sk>"], "style": { "height": "380px" } },
{ "id": "col-by-status", "type": "LayoutCard", "items": ["<chart item sk>"], "style": { "height": "380px" } }
] }
]
}
}| Field | What it does |
|---|---|
column type | Must be "LayoutCard". A column without it is not drawn at all. |
column items | The item's sk, not its name. Only the first item is shown. |
column title | The card's heading. When it is left out, the item's title is used. A section's title is not shown. |
column style.height | 160px for a number, 380px for a chart, 400px to 600px for a table. |
column valuesOnly | The result fields to show as large figures (step 3). |
filters.dateRange | preset: one of all, this-month, last-month, last-30, last-90, this-year or custom. A custom range takes startDate and endDate (YYYY-MM-DD). With no range, the dashboard covers all time. |
The Data Studio guidance built into the AI is wrong here. It says items may hold an item's name and shows columns with no type. Both were tested on a dashboard:
- A name made the card read "This card couldn't be loaded right now", because the card fetches its item by id and
GET /repository/get/dataviz_item/<name>answers 400 (websitemintlayout/layout-card.tsx69). - A column without
typewas left off the screen (layout/layout-column.tsx81).
Use "type": "LayoutCard" and the item's sk every time.
Adding to a dashboard that already exists. Read it with get_record on dataviz, which takes its name or its sk. Add a column to a section, or add a new section. Then write back the whole sections array with update_record_partial, using { "data.sections": [...] }. If you use update_record, send the dashboard's complete data, because it replaces all of it. Before changing a dashboard someone else made, say what will change and wait for a yes.
Hands on: the dashboard's sk and name.
Check: navigate to dashboard-builder with id: "<sk or name>". The dashboard opens at Dashboard Builder (/app/dashboard?id=<sk>) in Viewing mode, with the date range bar at the top. Every card should show figures. If one doesn't:
- "pick what this card shows": the column has no item.
- "This card couldn't be loaded right now": the item reference is wrong, or its query fails.
- An empty table or chart: the query matched nothing for this date range. Check its dates (step 2).
5. Data Studio: changes while the person is looking at it
When the person has Data Studio open, the AI's messages say what is on their screen: the open dashboard, or the open query with its rows. update_data with path: "/dashboard" changes what they see without saving anything.
- Query editor open: send
queryText(the pipeline as a string),selectedCollection,chartTypeorfieldMapping. The editor runs the new query at once. - Dashboard open: fields sent replace the dashboard's own fields.
sectionsis replaced as a whole. - Nothing is saved until the person presses Save, or you use
update_record.
The run that follows update_data doesn't fill {{startDate}} and {{endDate}} (websitemint data-viz-store.ts 369), so a query that uses them shows no rows there. Pressing Run in the editor fills them in from the range shown beside it.
Changing or deleting a chart or dashboard
- In Studio, open a dashboard and switch to Editing. The side panel's Queries and Other tabs list the saved items and dashboards, each with a bin icon: click it twice. Through the AI, use
delete_recordondataviz_itemordataviz, withconfirmed: true. - A delete is permanent. These records don't go to Trash. Tested: a deleted dashboard and its items were gone, and nothing was in Trash. See Bring back deleted records.
- A chart used on a dashboard: deleting it makes that card fail. Find the dashboards that use it first (
find_recordsondataviz, looking for itsskinsections). - A broadcast list. A
dataviz_itemof typequerymay be a broadcast's dynamic list (query:<sk>). Changing or deleting it changes who that broadcast reaches. See Broadcast audience from a filter.
Stop: delete only after the person says yes to the exact items. Name each item, and the dashboards and broadcasts that use it.
Exporting. A chart's download icon saves it as a picture. Duplicate copies a query or a dashboard. The copy is kept only once it is saved.
When it's done
Tell the person:
- the answer, the dates it covers, and its change on the period before;
- where the figure came from: which Analytic Center dashboard, or which collection and query;
- what each figure counts, for example that revenue leaves out unpaid, cancelled and refunded orders;
- anything you saved: each chart's title, and the dashboard's name and date range.
Open the Analytic Center tab, or the dashboard in Dashboard Builder.
What it can't do yet
- No scheduled or emailed reports. Nothing sends a dashboard or a report on a timetable. An AI employee can be given a routine that writes one: see Give an AI employee a standing job.
- No real export.
POST /analytics/exportreturns the same figures as JSON whateverformatis asked for, so there is no CSV or PDF (appengineanalytics/analytics.service.ts2736). Charts can be saved as pictures. - A text item isn't formatted. Its
content.textshows as one cell of a table, and Markdown isn't rendered (from the code,layout/layout-table.tsx). - One item per card, and a column's
widthand a section'stitleare ignored. - The chart options box isn't saved. The query editor's chart override is not written by Save. Set
viewConfig.chartOverrideon the record instead. - No undo for a deleted chart or dashboard.
- A dashboard's date range is the only filter. It has no status or product filter. Put those in the query.
Don'ts
- Don't build a query for something an Analytic Center dashboard already reports. Its definitions are the business's numbers.
- Don't save a query that hasn't returned real rows, and save nothing the person didn't ask to keep.
- Don't compare
createdateormodifydatewith text dates. Use$toDate. - Don't reference items by name on a dashboard, and don't leave out
"type": "LayoutCard". - Don't call a figure "revenue" when it includes unpaid or refunded orders.
- Don't delete a chart or a query without checking the dashboards and broadcasts that use it.
- Don't change someone else's dashboard without a yes.