docs
/
AI skills

Dashboards and reports

Answer "how are we doing" from the reports the platform already keeps (the Analytic Center's ten dashboards and each Studio section's figures), and build a dashboard of your own in the Dashboard Builder. Its queries are tested against real rows first, saved as charts, tables and number cards, given a date range that really filters, laid out and opened for the person. It only reads; nothing is sent.

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:

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).

DashboardGETIts overviewAlso 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, totalCommentstopPosts, engagementTrends, categoryPerformance, authorPerformance; only for a site whose blog is on
Storefront/analytics/storefronttotalRevenue, totalOrders, paidOrders, avgOrderValue, returnCustomers, pendingOrdersproductPerformance, channelPerformance, salesTrends, revenueTrends, ordersByStatus, returns, customerSegments
Leads/analytics/leads (+ pipelineId, onlyPipeline, tz)totalLeads, qualifiedLeads, conversionRate, avgDealSize, pipelineValueleadSources, funnelData, pipelineFunnels, leadTrends, campaignPerformance
Tickets/analytics/ticketstotalTickets, openTickets, resolvedTickets, avgResolutionTime, customerSatisfactionticketsByPriority, ticketsByCategory, agentPerformance
Workflow/analytics/workflowtotalTasks, completedTasks, openTasks, overdueTaskstasksByStatus, assigneePerformance, workloadDistribution
Automation/analytics/automationactiveWorkflows, totalExecutions, successRate, failedExecutionsworkflowPerformance, errorAnalysis, executionTrends
User accounts/analytics/userstotalUsers, newUsers, activeUsers, payingUsers, churnRateuserGrowth, subscriptionTiers
Email/analytics/emailtotalSent, delivered, opened, clicked, bounced, unsubscribedrates, campaignMetrics, dailySends
Live View/analytics/live-view (minutes, default 15)visitors on the site noweach visitor's journey: /analytics/live-view/<deviceId>/journey
  • The period before. Every dashboard except Live View also answers with a period and a comparison: the same length of time just before it. Each overview figure carries its previous value, changePercent, direction (up, down or flat) and sentiment (good or bad; 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 endDate includes that whole day in UTC. Leads also takes tz, 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.status is paid, paid-partial, overpaid, processing, shipped, in_transit, out_for_delivery, delivered or completed. An unpaid, cancelled or refunded order is counted as an order but not as revenue. An order's amount is data.amount, or data.total when there is no amount (analytics/analytics-query.ts 92–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, for home, account, config, database, build-studio, dam, crm, store, events, community, finance, logistics and ai-automation. For example, store gives 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_schema on the collection. A record's own fields sit under data. ($data.status, $data.amount). createdate, modifydate, state and sk sit 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 $ifNull so 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": "" }
      }
    }
  }
}
FieldWhat it is
nameRequired and unique: 3 to 80 letters, digits, - or _. Check with find_records that it isn't taken.
typechart 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.collectionThe collection the pipeline runs on.
content.mongoQueryThe pipeline as a JSON string, not an array.
viewConfig.chartTypebar, line, area, pie, doughnut, scatter, radar, gauge, funnel, heatmap, treemap, sunburst, boxplot or sankey
viewConfig.fieldMappingcategoryField: 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.chartOverrideOptional 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 categoryField and one value.
  • Heatmap: categoryField across, seriesField down, one value.
  • Sankey: categoryField is the source and seriesField the 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" } }
      ] }
    ]
  }
}
FieldWhat it does
column typeMust be "LayoutCard". A column without it is not drawn at all.
column itemsThe item's sk, not its name. Only the first item is shown.
column titleThe card's heading. When it is left out, the item's title is used. A section's title is not shown.
column style.height160px for a number, 380px for a chart, 400px to 600px for a table.
column valuesOnlyThe result fields to show as large figures (step 3).
filters.dateRangepreset: 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 (websitemint layout/layout-card.tsx 69).
  • A column without type was left off the screen (layout/layout-column.tsx 81).

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, chartType or fieldMapping. The editor runs the new query at once.
  • Dashboard open: fields sent replace the dashboard's own fields. sections is 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_record on dataviz_item or dataviz, with confirmed: 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_records on dataviz, looking for its sk in sections).
  • A broadcast list. A dataviz_item of type query may 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/export returns the same figures as JSON whatever format is asked for, so there is no CSV or PDF (appengine analytics/analytics.service.ts 2736). Charts can be saved as pictures.
  • A text item isn't formatted. Its content.text shows 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 width and a section's title are ignored.
  • The chart options box isn't saved. The query editor's chart override is not written by Save. Set viewConfig.chartOverride on 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 createdate or modifydate with 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.