Guide

Calendar Analytics from the Terminal

Measure meeting load from the terminal with calendar analytics. Count total hours, recurring vs one-off meetings, and busiest days using jq and Nylas CLI.

Written by Caleb Geene Director, Site Reliability Engineering

VerifiedCLI 3.1.16 · Google Calendar · last tested June 9, 2026

You spend hours in meetings every week. How many hours exactly? Which day is heaviest? What share are standing syncs you could cancel? Those questions are hard to answer from a calendar UI, but straightforward from the terminal. Pipe your calendar data through jq and you have the numbers in seconds.

How do you connect your calendar for analytics?

Calendar analytics starts with a connected account. Nylas CLI stores credentials locally after a one-time nylas auth login flow that takes under 60 seconds and supports Google Calendar, Outlook, Exchange, Yahoo, iCloud, and IMAP providers through the same interface. Once connected, every analytics command reads from the same local credentials — no tokens to manage per run.

# Authenticate once — opens OAuth in your browser
nylas auth login

# Confirm the connection shows your calendar account
nylas calendar list

For CI or cron environments, skip the interactive flow and use environment variables instead: NYLAS_API_KEY, NYLAS_GRANT_ID, and NYLAS_DISABLE_KEYRING=1. See the getting started guide for headless setup.

How do you pull 28 days of events as JSON?

The nylas calendar events list command fetches upcoming events from all connected calendars. Pass --days 28 to cover a full 4-week window and --json to get structured output. A 28-day window typically returns 50–200 events for an active calendar, well within Nylas rate limits in a single request.

# Fetch 28 days and save to a file for repeated analysis
nylas calendar events list --days 28 --json > events.json

# Or pipe directly into jq
nylas calendar events list --days 28 --json | jq length

Saving to a file lets you run multiple jq queries against the same snapshot without hitting the API again. This is the right pattern for a weekly report script that runs several aggregations in sequence.

How do you count total meeting hours?

Total meeting hours is the sum of (end_time - start_time) across all events with valid timestamps, divided by 3600 to convert seconds to hours. The expression below filters out all-day events (which use when.date rather than Unix timestamps), guards against zero-duration entries, and rounds to one decimal place.

# Total meeting hours over the 28-day window
nylas calendar events list --days 28 --json | jq '
  [
    .[]
    | select(.when.start_time? and .when.end_time?)
    | select(.when.end_time > .when.start_time)
    | (.when.end_time - .when.start_time)
  ]
  | add // 0
  | . / 3600
  | . * 10
  | round
  | . / 10
'

A typical result for an active calendar is 20–40 hours over 28 days. If the number surprises you — in either direction — that's the point. The CLI reads from your actual calendar, not a self-reported estimate.

How do you count total meetings and average per day?

Event count and daily average are the two baseline metrics for calendar load. The count covers every event with valid timestamps — including short 15-minute check-ins. Dividing by 20 (working days in a 28-day window) gives the daily average. According to Microsoft's 2023 Work Trend Index, the average knowledge worker attends 25.6 meetings per week — roughly 5.1 per day. Compare your output against that baseline to calibrate.

# Count meetings and compute daily average
nylas calendar events list --days 28 --json | jq '
  [
    .[]
    | select(.when.start_time? and .when.end_time?)
    | select(.when.end_time > .when.start_time)
  ]
  | {
      total_meetings: length,
      avg_per_working_day: (length / 20 * 10 | round / 10)
    }
'

How do you split recurring vs one-off meetings?

Recurring meetings carry a non-null recurrence field in the event JSON — typically an RRULE string like FREQ=WEEKLY;BYDAY=MO. One-off meetings have a null or absent recurrence field. The split below counts both types in one pass and computes the recurring percentage, which tells you how much of your meeting time is committed overhead versus reactive scheduling.

# Recurring vs one-off breakdown
nylas calendar events list --days 28 --json | jq '
  [
    .[]
    | select(.when.start_time? and .when.end_time?)
    | select(.when.end_time > .when.start_time)
  ] as $events
  | {
      recurring: ($events | map(select(.recurrence != null)) | length),
      one_off:   ($events | map(select(.recurrence == null)) | length),
      total:     ($events | length),
      recurring_pct: (
        if ($events | length) > 0 then
          ($events | map(select(.recurrence != null)) | length) * 100
          / ($events | length)
          | . * 10 | round | . / 10
        else 0 end
      )
    }
'

A recurring-meeting ratio above 70% often signals calendar sprawl — standing syncs accumulated over months that preempt new work. Below 30% can indicate a reactive team that under-invests in structured coordination.

How do you find your busiest meeting day of the week?

The busiest day is the weekday with the highest total meeting minutes across the full 28-day window. Aggregating across 4 weeks smooths out anomalies: one unusually heavy Thursday doesn't mean Thursdays are always heavy, but 4 consecutive heavy Thursdays does. The jq expression converts each event's Unix start timestamp to a weekday name using todate | split("T")[0] | strptime("%Y-%m-%d") | strftime("%A"), then groups and sums minutes per day.

# Total meeting minutes per weekday, sorted descending
nylas calendar events list --days 28 --json | jq '
  [
    .[]
    | select(.when.start_time? and .when.end_time?)
    | select(.when.end_time > .when.start_time)
    | {
        day: (.when.start_time | todate | split("T")[0]
              | strptime("%Y-%m-%d") | strftime("%A")),
        minutes: ((.when.end_time - .when.start_time) / 60)
      }
  ]
  | group_by(.day)
  | map({
      day: .[0].day,
      total_minutes: (map(.minutes) | add | round)
    })
  | sort_by(-.total_minutes)
'

The output is a ranked list — Monday through Friday sorted by total minutes. Your heaviest day is the first entry. If it's more than 30 minutes heavier than the second-place day, that pattern is consistent enough to act on.

How do you combine all metrics into one report?

Running all four analytics queries from separate commands works fine for exploration, but a single script that produces a structured JSON report is more useful for weekly automation. The script below fetches events once, saves them locally, and runs all aggregations in one pass — total hours, meeting count, recurring split, and busiest day. It takes under 5 seconds on a 28-day window.

#!/usr/bin/env bash
# calendar-report.sh — weekly meeting analytics

set -euo pipefail
SNAPSHOT=$(nylas calendar events list --days 28 --json)

echo "$SNAPSHOT" | jq '
  [
    .[]
    | select(.when.start_time? and .when.end_time?)
    | select(.when.end_time > .when.start_time)
  ] as $ev
  | {
      window_days: 28,
      total_meetings: ($ev | length),
      avg_per_working_day: (($ev | length) / 20 * 10 | round | . / 10),
      total_hours: (
        [$ev[] | (.when.end_time - .when.start_time)]
        | add // 0 | . / 3600 * 10 | round | . / 10
      ),
      recurring_pct: (
        if ($ev | length) > 0 then
          ($ev | map(select(.recurrence != null)) | length) * 100
          / ($ev | length) | . * 10 | round | . / 10
        else 0 end
      ),
      busiest_day: (
        [$ev[]
          | {
              day: (.when.start_time | todate | split("T")[0]
                    | strptime("%Y-%m-%d") | strftime("%A")),
              mins: ((.when.end_time - .when.start_time) / 60)
            }
        ]
        | group_by(.day)
        | map({day: .[0].day, total: (map(.mins) | add | round)})
        | sort_by(-.total)
        | .[0].day
      )
    }
'

Run this as a cron job every Monday morning to get last week's report in your terminal. Store each output as a dated JSON file (~/.calendar-stats/$(date +%Y-%W).json) and you have a 52-week archive in roughly 50 KB of total storage.

Tested on Nylas CLI 3.1.16 with Google Calendar

These commands were verified against Nylas CLI 3.1.16 on macOS 15 (Sequoia) with a Google Calendar account on 2026-06-09. The jq expressions require jq 1.6 or later, which ships with macOS Homebrew (brew install jq) and all major Linux distributions. The strptime and strftime builtins are not available in jq 1.5.

Provider-side behavior for Outlook Calendar and Exchange is described from documented provider behavior, not from a verified end-to-end test on each backend — verify locally before deploying provider-specific analytics rules.

Why measure meeting time from the terminal?

Calendar analytics is the practice of aggregating event data — durations, recurrence patterns, participant counts, and day-of-week distribution — to understand where a calendar owner's time actually goes. A 4-week window covers roughly 20 working days, enough to spot patterns like recurring 1-hour syncs that collectively consume 6 hours per week.

Most calendar interfaces show individual events but offer no summary view. You can see that Tuesday has 5 meetings, but you can't easily answer: how many total hours did I spend in meetings last month? What fraction were recurring? Which day averaged the most meeting time? Terminal-based analytics answers all three in under 10 seconds.

According to Microsoft's 2023 Work Trend Index, the average knowledge worker attends 25.6 meetings per week. That's roughly 17–20 hours of scheduled time. If even a quarter of those meetings are optional or could be async, the potential reclaim is 4–5 hours per week. You can't reclaim time you haven't measured.

How do recurring and one-off meetings differ in analytics?

A recurring meeting is any event where the JSON output includes a non-null `recurrence` field — typically an RRULE string like `FREQ=WEEKLY;BYDAY=MO`. One-off meetings have a null or absent recurrence field. Separating the two tells you how much of your meeting time is committed overhead (recurring) versus reactive scheduling (ad hoc).

High recurring-meeting ratios (over 60%) often signal calendar sprawl: standing syncs added over months that no longer have a clear owner. High one-off ratios can mean reactive work or a team that skips planning. Neither is inherently bad, but the number helps you ask the right question.

The jq split below counts both types in a single pass over the events JSON, so you don't need to pull the data twice. One 7-day fetch costs 1 API call under Nylas rate limits.

How do you calculate meeting duration from event JSON?

Each event in the `nylas calendar events list --json` output carries `when.start_time` and `when.end_time` as Unix timestamps (seconds since epoch). Duration in seconds is `end_time - start_time`. Convert to minutes by dividing by 60, or to hours by dividing by 3600. All-day events use `when.date` instead of timestamps — exclude them from duration math or treat them as 8-hour blocks depending on your use case.

jq can do integer arithmetic directly, so no external tool is needed. The expression `.when.end_time - .when.start_time` runs inline inside a `map` or `reduce`. For a 30-minute event, start=1749600000 and end=1749601800 gives 1800 seconds, or 30 minutes exactly.

Watch for events where `end_time` is missing or equals `start_time`. Some all-day events from Google Calendar serialize as zero-duration. Add a guard (`select(.when.end_time > .when.start_time)`) before summing to avoid dividing by zero or inflating the count.

How do you find the busiest day of the week?

The busiest day is the day-of-week label with the highest total meeting minutes across the analysis window. jq extracts the day label from the ISO date string in `when.start_time` by converting the Unix timestamp to a date, then grouping by day name. Over 4 weeks, Monday might accumulate 360 minutes while Wednesday hits 480 — a clear signal that Wednesday is your heaviest sync day.

Day-of-week analysis is more useful than single-day analysis because it averages out anomalies. One unusually heavy Thursday doesn't mean Thursdays are always heavy; 4 Thursdays in a row at 3+ hours each does. Aggregating across the full window before ranking produces a more reliable pattern.

The jq expression uses `strftime('%A')` on the event's date. This requires jq 1.6 or later, which is the version bundled with macOS 12+ via Homebrew and available on all major Linux distributions.

How do you run calendar analytics on a schedule?

Calendar analytics becomes more useful when it runs automatically — a weekly Monday-morning report showing last week's meeting load, or a daily terminal header that shows how many meeting hours remain today. Both patterns use the same `nylas calendar events list --json | jq` pipeline, wrapped in a cron job or shell profile hook.

The CLI reads auth from environment variables (`NYLAS_API_KEY`, `NYLAS_GRANT_ID`) when `NYLAS_DISABLE_KEYRING=1` is set, which makes it safe to run inside cron, GitHub Actions, or Docker containers without an interactive keyring. A weekly report job takes under 5 seconds and produces less than 1 KB of output per run.

Store the JSON output to a file if you want trend data across multiple weeks. A weekly snapshot saved to `~/.calendar-stats/YYYY-WW.json` gives you a 52-week archive with roughly 50 KB of total storage. Compare this week against last week with a second `jq` pass to spot sudden changes in meeting load.

How do you extend analytics to a whole team?

Team-level analytics loops over multiple grant IDs and aggregates the results. Each Nylas grant represents one connected calendar account. If your organization has 10 engineers with connected accounts, 10 grant IDs produce 10 event lists. A simple shell loop collects all 10 into a single JSON array and the same jq expressions work unchanged.

Team analytics surfaces coordination costs: total hours the team spent in meetings where 3 or more people overlapped, the 5 recurring meetings with the most combined attendee-hours, or the day of week with the highest team-wide meeting density. These numbers are hard to get from a calendar UI but straightforward with a pipe.

Access controls matter here. Each grant only returns events the connected account can see. If a manager's calendar is private, the loop returns only shared events. Build team analytics on shared or department-level calendars, or ensure each team member has opted in to the connected account.

What meeting patterns are worth acting on?

Three thresholds are worth tracking. First, more than 4 meetings per day on average signals fragmented time — fewer than 4 interruptions per day is associated with sustained focus in workplace research. Second, recurring meetings accounting for over 70% of total meeting time suggests the calendar has accumulated standing syncs that preempt new work. Third, any single day consistently averaging more than 3 hours of meetings is worth auditing for consolidation.

Context matters as much as numbers. A product launch week with 30 hours of meetings is expected; a routine week with 30 hours is a warning sign. Use the weekly snapshot pattern to compare against your own baseline rather than a fixed threshold.

The analytics commands don't suggest actions — that's deliberate. The numbers are inputs to a decision, not the decision itself. Use them to identify which recurring meetings to cancel, which days to protect as focus time, and which meeting types to convert to async updates.

References

Next steps