Skip to main content
Glama

query_metric_aggregates

Aggregate Klaviyo metrics by measurement, timeframe, and dimensions to analyze marketing performance data.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
metric_idYesID of the metric to aggregate
measurementYesMeasurement to use (e.g., count, sum, unique)
timeframeYesTimeframe to use (e.g., last_30_days, this_month)
group_byNoDimensions to group by
start_dateNoCustom start date (ISO format, overrides timeframe)
end_dateNoCustom end date (ISO format, overrides timeframe)

Implementation Reference

  • The handler function for the 'query_metric_aggregates' tool. It validates inputs, builds the API payload for Klaviyo's metric-aggregate endpoint, handles timeframe and grouping parameters with custom logic and fallbacks, calls the API, and returns the results or error.
    async (params) => {
      try {
        logger.info(`Querying metric aggregates for metric ID: ${params.metric_id}`);
    
        // Validate measurement
        if (!VALID_MEASUREMENTS.includes(params.measurement)) {
          logger.warn(`Invalid measurement: ${params.measurement}. Using 'count' instead.`);
          params.measurement = 'count';
        }
    
        // Create payload
        const payload = {
          data: {
            type: "metric-aggregate",
            attributes: {
              metric_id: params.metric_id,
              measurements: [params.measurement],
              interval: "day",
              filter: [],
              timezone: "UTC"
            }
          }
        };
    
        // Use custom dates if provided, otherwise use timeframe parameter
        if (params.start_date && params.end_date) {
          // Use custom date range
          const startDateStr = params.start_date.split('T')[0];
          const endDateStr = params.end_date.split('T')[0];
    
          const dateFilters = FILTER_TEMPLATES.dateRange(
            `${startDateStr}T00:00:00`,
            `${endDateStr}T23:59:59`
          );
    
          payload.data.attributes.filter.push(...dateFilters);
          logger.debug(`Using custom date range: ${startDateStr} to ${endDateStr}`);
        } else if (TIMEFRAME_OPTIONS[params.timeframe]) {
          // Use predefined timeframe if valid
          if (params.timeframe === "last_30_days") {
            const endDate = new Date();
            const startDate = new Date();
            startDate.setDate(startDate.getDate() - 30);
    
            const startDateStr = startDate.toISOString().split('T')[0];
            const endDateStr = endDate.toISOString().split('T')[0];
    
            const dateFilters = FILTER_TEMPLATES.dateRange(
              `${startDateStr}T00:00:00`,
              `${endDateStr}T23:59:59`
            );
    
            payload.data.attributes.filter.push(...dateFilters);
            logger.debug(`Using timeframe: ${params.timeframe} (${startDateStr} to ${endDateStr})`);
          } else if (params.timeframe === "this_month") {
            const now = new Date();
            const firstDay = new Date(now.getFullYear(), now.getMonth(), 1);
    
            const startDateStr = firstDay.toISOString().split('T')[0];
            const endDateStr = now.toISOString().split('T')[0];
    
            const dateFilters = FILTER_TEMPLATES.dateRange(
              `${startDateStr}T00:00:00`,
              `${endDateStr}T23:59:59`
            );
    
            payload.data.attributes.filter.push(...dateFilters);
            logger.debug(`Using timeframe: ${params.timeframe} (${startDateStr} to ${endDateStr})`);
          } else {
            logger.debug(`Using predefined timeframe: ${params.timeframe}`);
            payload.data.attributes.timeframe = {
              key: params.timeframe
            };
          }
        } else {
          // Default to last 7 days if timeframe is not recognized
          const endDate = new Date();
          const startDate = new Date();
          startDate.setDate(startDate.getDate() - 7);
    
          const startDateStr = startDate.toISOString().split('T')[0];
          const endDateStr = endDate.toISOString().split('T')[0];
    
          const dateFilters = FILTER_TEMPLATES.dateRange(
            `${startDateStr}T00:00:00`,
            `${endDateStr}T23:59:59`
          );
    
          payload.data.attributes.filter.push(...dateFilters);
          logger.debug(`Using default 7-day range: ${startDateStr} to ${endDateStr}`);
        }
    
        if (params.group_by) {
          payload.data.attributes.by = params.group_by;
          logger.debug(`Grouping by: ${params.group_by.join(', ')}`);
        }
    
        logger.debug('Metric aggregates request payload', payload);
    
        // Define the fallback function
        const fallbackFn = async (error) => {
          logger.warn(`Error querying metric aggregates: ${error.message}. Attempting fallback.`);
          
          // Simplified fallback payload with minimal parameters
          const fallbackPayload = {
            data: {
              type: "metric-aggregate",
              attributes: {
                metric_id: params.metric_id,
                measurements: ["count"], // Default to count measurement
                interval: "day",
                filter: [],
                timezone: "UTC"
              }
            }
          };
    
          // Use a default time range for the fallback (last 7 days)
          const endDate = new Date();
          const startDate = new Date();
          startDate.setDate(startDate.getDate() - 7);
    
          const startDateStr = startDate.toISOString().split('T')[0];
          const endDateStr = endDate.toISOString().split('T')[0];
    
          const dateFilters = FILTER_TEMPLATES.dateRange(
            `${startDateStr}T00:00:00`,
            `${endDateStr}T23:59:59`
          );
    
          fallbackPayload.data.attributes.filter.push(...dateFilters);
    
          logger.debug('Metric aggregates fallback payload', fallbackPayload);
          
          const fallbackResults = await klaviyoClient.post('/metric-aggregates/', fallbackPayload);
          logger.info(`Successfully retrieved basic metric aggregates for metric ID: ${params.metric_id} using fallback`);
          
          return fallbackResults;
        };
    
        // Ensure endpoint has trailing slash for consistency
        const results = await klaviyoClient.post('/metric-aggregates/', payload, fallbackFn);
    
        logger.info(`Successfully retrieved metric aggregates for metric ID: ${params.metric_id}`);
    
        return {
          content: [{ type: "text", text: JSON.stringify(results, null, 2) }]
        };
      } catch (error) {
        logger.error(`Failed to query metric aggregates (including fallback attempt): ${error.message}`, {
          metricId: params.metric_id
        });
    
        return {
          content: [{ type: "text", text: `Error querying metric aggregates (including fallback attempt): ${error.message}` }],
          isError: true
        };
      }
  • Zod schema defining the input parameters for the query_metric_aggregates tool, including metric_id, measurement, timeframe, group_by, and optional date ranges.
    {
      metric_id: z.string().describe("ID of the metric to aggregate"),
      measurement: z.string().describe("Measurement to use (e.g., count, sum, unique)"),
      timeframe: z.string().describe("Timeframe to use (e.g., last_30_days, this_month)"),
      group_by: z.array(z.string()).optional().describe("Dimensions to group by"),
      start_date: z.string().optional().describe("Custom start date (ISO format, overrides timeframe)"),
      end_date: z.string().optional().describe("Custom end date (ISO format, overrides timeframe)")
    },
  • The server.tool call that registers the query_metric_aggregates tool within the registerReportingTools function.
    server.tool(
      "query_metric_aggregates",
      {
        metric_id: z.string().describe("ID of the metric to aggregate"),
        measurement: z.string().describe("Measurement to use (e.g., count, sum, unique)"),
        timeframe: z.string().describe("Timeframe to use (e.g., last_30_days, this_month)"),
        group_by: z.array(z.string()).optional().describe("Dimensions to group by"),
        start_date: z.string().optional().describe("Custom start date (ISO format, overrides timeframe)"),
        end_date: z.string().optional().describe("Custom end date (ISO format, overrides timeframe)")
      },
      async (params) => {
        try {
          logger.info(`Querying metric aggregates for metric ID: ${params.metric_id}`);
    
          // Validate measurement
          if (!VALID_MEASUREMENTS.includes(params.measurement)) {
            logger.warn(`Invalid measurement: ${params.measurement}. Using 'count' instead.`);
            params.measurement = 'count';
          }
    
          // Create payload
          const payload = {
            data: {
              type: "metric-aggregate",
              attributes: {
                metric_id: params.metric_id,
                measurements: [params.measurement],
                interval: "day",
                filter: [],
                timezone: "UTC"
              }
            }
          };
    
          // Use custom dates if provided, otherwise use timeframe parameter
          if (params.start_date && params.end_date) {
            // Use custom date range
            const startDateStr = params.start_date.split('T')[0];
            const endDateStr = params.end_date.split('T')[0];
    
            const dateFilters = FILTER_TEMPLATES.dateRange(
              `${startDateStr}T00:00:00`,
              `${endDateStr}T23:59:59`
            );
    
            payload.data.attributes.filter.push(...dateFilters);
            logger.debug(`Using custom date range: ${startDateStr} to ${endDateStr}`);
          } else if (TIMEFRAME_OPTIONS[params.timeframe]) {
            // Use predefined timeframe if valid
            if (params.timeframe === "last_30_days") {
              const endDate = new Date();
              const startDate = new Date();
              startDate.setDate(startDate.getDate() - 30);
    
              const startDateStr = startDate.toISOString().split('T')[0];
              const endDateStr = endDate.toISOString().split('T')[0];
    
              const dateFilters = FILTER_TEMPLATES.dateRange(
                `${startDateStr}T00:00:00`,
                `${endDateStr}T23:59:59`
              );
    
              payload.data.attributes.filter.push(...dateFilters);
              logger.debug(`Using timeframe: ${params.timeframe} (${startDateStr} to ${endDateStr})`);
            } else if (params.timeframe === "this_month") {
              const now = new Date();
              const firstDay = new Date(now.getFullYear(), now.getMonth(), 1);
    
              const startDateStr = firstDay.toISOString().split('T')[0];
              const endDateStr = now.toISOString().split('T')[0];
    
              const dateFilters = FILTER_TEMPLATES.dateRange(
                `${startDateStr}T00:00:00`,
                `${endDateStr}T23:59:59`
              );
    
              payload.data.attributes.filter.push(...dateFilters);
              logger.debug(`Using timeframe: ${params.timeframe} (${startDateStr} to ${endDateStr})`);
            } else {
              logger.debug(`Using predefined timeframe: ${params.timeframe}`);
              payload.data.attributes.timeframe = {
                key: params.timeframe
              };
            }
          } else {
            // Default to last 7 days if timeframe is not recognized
            const endDate = new Date();
            const startDate = new Date();
            startDate.setDate(startDate.getDate() - 7);
    
            const startDateStr = startDate.toISOString().split('T')[0];
            const endDateStr = endDate.toISOString().split('T')[0];
    
            const dateFilters = FILTER_TEMPLATES.dateRange(
              `${startDateStr}T00:00:00`,
              `${endDateStr}T23:59:59`
            );
    
            payload.data.attributes.filter.push(...dateFilters);
            logger.debug(`Using default 7-day range: ${startDateStr} to ${endDateStr}`);
          }
    
          if (params.group_by) {
            payload.data.attributes.by = params.group_by;
            logger.debug(`Grouping by: ${params.group_by.join(', ')}`);
          }
    
          logger.debug('Metric aggregates request payload', payload);
    
          // Define the fallback function
          const fallbackFn = async (error) => {
            logger.warn(`Error querying metric aggregates: ${error.message}. Attempting fallback.`);
            
            // Simplified fallback payload with minimal parameters
            const fallbackPayload = {
              data: {
                type: "metric-aggregate",
                attributes: {
                  metric_id: params.metric_id,
                  measurements: ["count"], // Default to count measurement
                  interval: "day",
                  filter: [],
                  timezone: "UTC"
                }
              }
            };
    
            // Use a default time range for the fallback (last 7 days)
            const endDate = new Date();
            const startDate = new Date();
            startDate.setDate(startDate.getDate() - 7);
    
            const startDateStr = startDate.toISOString().split('T')[0];
            const endDateStr = endDate.toISOString().split('T')[0];
    
            const dateFilters = FILTER_TEMPLATES.dateRange(
              `${startDateStr}T00:00:00`,
              `${endDateStr}T23:59:59`
            );
    
            fallbackPayload.data.attributes.filter.push(...dateFilters);
    
            logger.debug('Metric aggregates fallback payload', fallbackPayload);
            
            const fallbackResults = await klaviyoClient.post('/metric-aggregates/', fallbackPayload);
            logger.info(`Successfully retrieved basic metric aggregates for metric ID: ${params.metric_id} using fallback`);
            
            return fallbackResults;
          };
    
          // Ensure endpoint has trailing slash for consistency
          const results = await klaviyoClient.post('/metric-aggregates/', payload, fallbackFn);
    
          logger.info(`Successfully retrieved metric aggregates for metric ID: ${params.metric_id}`);
    
          return {
            content: [{ type: "text", text: JSON.stringify(results, null, 2) }]
          };
        } catch (error) {
          logger.error(`Failed to query metric aggregates (including fallback attempt): ${error.message}`, {
            metricId: params.metric_id
          });
    
          return {
            content: [{ type: "text", text: `Error querying metric aggregates (including fallback attempt): ${error.message}` }],
            isError: true
          };
        }
      },
      { description: "Query aggregated metric data for custom analytics reporting" }
    );
  • src/server.js:38-38 (registration)
    Top-level call to registerReportingTools, which includes the registration of the query_metric_aggregates tool among other reporting tools.
    registerReportingTools(server);
Behavior1/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Tool has no description.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness1/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Tool has no description.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness1/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Tool has no description.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters1/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Tool has no description.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose1/5

Does the description clearly state what the tool does and how it differs from similar tools?

Tool has no description.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines1/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Tool has no description.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/ivan-rivera-projects/Klaviyo-MCP-Server-Enhanced'

If you have feedback or need assistance with the MCP directory API, please join our Discord server