appscript.dev
Automation Intermediate Gmail Calendar Sheets

Email yourself a morning briefing

Combine today's calendar, the weather, and your task list into one 7am digest.

Published Sep 2, 2025

Starting the day usually means opening three apps: the calendar to see what’s on, a weather app to decide what to wear, and wherever the to-do list lives. By the time you’ve checked all three, you’ve also been pulled into email and the first ten minutes are gone.

At Northwind, Awadesh wanted that picture assembled for him. This script runs before the working day, pulls today’s meetings from Google Calendar, the current weather in London from a free API, and the open rows from a Tasks sheet, and emails it all as one tidy briefing. One message, three sources, no app-hopping — and because it’s email, it’s already on every device.

What you’ll need

  • A Google Calendar — the script reads your default calendar.
  • A Google Sheet named so you can find it, with a tab whose columns include title and status. Anything with a status other than done is treated as an open task.
  • The sheet’s ID, pasted into TASKS_SHEET in the config block.
  • No API key — Open-Meteo is free and keyless.

The script

// The spreadsheet holding your task list.
const TASKS_SHEET = '1abcTasksSheetId';

// Coordinates for the weather lookup. These point at central London —
// change them to wherever you actually are.
const WEATHER_LATITUDE = 51.5;
const WEATHER_LONGITUDE = -0.12;

/**
 * Builds a morning briefing from today's calendar, the weather, and the
 * open tasks sheet, then emails it to you. Runs on a daily trigger.
 */
function morningBriefing() {
  const today = new Date();

  // 1. Gather the three data sources.
  const events = todaysEvents(today);
  const weather = londonWeather();
  const tasks = openTasks();

  // 2. Assemble the briefing as plain text, with a fallback line for
  //    each section when there's nothing to report.
  const body = [
    `Good morning. ${Utilities.formatDate(today, 'GMT', 'EEEE, d MMMM')}`,
    '',
    '── Weather ──',
    weather,
    '',
    '── Today ──',
    events.length ? events.map(formatEvent).join('\n') : 'Nothing scheduled.',
    '',
    '── Open tasks ──',
    tasks.length ? tasks.map((t) => `• ${t}`).join('\n') : 'Clear.',
  ].join('\n');

  // 3. Send it to yourself.
  GmailApp.sendEmail(Session.getActiveUser().getEmail(), 'Morning briefing', body);
}

/**
 * Returns all events on the default calendar for the given day,
 * from midnight to midnight.
 */
function todaysEvents(d) {
  const start = new Date(d);
  start.setHours(0, 0, 0, 0);
  const end = new Date(d);
  end.setHours(23, 59, 59, 999);
  return CalendarApp.getDefaultCalendar().getEvents(start, end);
}

/**
 * Formats a single calendar event as a bullet with its start time.
 */
function formatEvent(e) {
  const t = Utilities.formatDate(e.getStartTime(), 'GMT', 'HH:mm');
  return `• ${t}  ${e.getTitle()}`;
}

/**
 * Fetches the current temperature from the keyless Open-Meteo API
 * and returns it as a short, readable line.
 */
function londonWeather() {
  const url = 'https://api.open-meteo.com/v1/forecast'
    + `?latitude=${WEATHER_LATITUDE}&longitude=${WEATHER_LONGITUDE}`
    + '&current=temperature_2m,weather_code';
  const res = UrlFetchApp.fetch(url, { muteHttpExceptions: true });

  // If the weather call fails, don't let it kill the whole briefing.
  if (res.getResponseCode() !== 200) return 'Weather unavailable.';

  const data = JSON.parse(res.getContentText());
  return `${data.current.temperature_2m}°C in London`;
}

/**
 * Reads the tasks sheet and returns the titles of every row whose
 * status is not "done".
 */
function openTasks() {
  const [header, ...rows] = SpreadsheetApp.openById(TASKS_SHEET)
    .getSheets()[0]
    .getDataRange()
    .getValues();

  // Map header names to column indexes so the code doesn't depend
  // on column order.
  const col = Object.fromEntries(header.map((h, i) => [h, i]));
  return rows
    .filter((r) => r[col.status] !== 'done')
    .map((r) => r[col.title]);
}

How it works

  1. morningBriefing is the entry point. It calls three helpers — one per data source — then stitches their output into a single email body.
  2. todaysEvents builds a midnight-to-midnight window for today and asks the default calendar for everything inside it. formatEvent turns each event into a bullet prefixed with its start time.
  3. londonWeather calls Open-Meteo, a free weather API that needs no key. It checks the HTTP status first: if the call fails, it returns Weather unavailable. rather than throwing, so a flaky weather service can’t stop the briefing going out.
  4. openTasks reads the tasks sheet and maps the header row to column indexes, so the script keeps working even if you reorder columns. It returns the title of every row whose status isn’t done.
  5. The body uses a ternary for each section, so an empty calendar shows Nothing scheduled. and an empty task list shows Clear. instead of a blank gap.
  6. GmailApp.sendEmail sends the finished briefing to your own address.

Example run

Suppose today is a Tuesday with two meetings, mild weather, and two open rows in the tasks sheet. The email body looks like this:

Good morning. Tuesday, 2 September

── Weather ──
14.2°C in London

── Today ──
• 10:00  Client kickoff — Riverside
• 15:30  Pipeline review

── Open tasks ──
• Send Q3 invoice to Acme
• Draft the September newsletter

On a clear day with nothing booked, the same three sections fall back to Nothing scheduled. and Clear. — still a complete briefing, just a quieter one.

Trigger it

This is a daily digest, so schedule it for first thing:

  1. In the Apps Script editor, open Triggers (the clock icon).
  2. Click Add Trigger.
  3. Choose morningBriefing, event source Time-driven, type Day timer, and the 6am–7am slot.
  4. Save, and approve the authorisation prompt — it asks for Calendar, Sheets, Gmail, and external request access, one for each source.

Watch out for

  • Time zones are easy to get wrong. The script formats everything with 'GMT', which suits London. If you’re elsewhere, swap 'GMT' for your zone (for example 'America/New_York') in both formatEvent and the date heading, or your meeting times will be off.
  • It reads the default calendar only. If your meetings live on a separate work calendar, swap getDefaultCalendar() for CalendarApp.getCalendarById('...').
  • All-day events have a start time too, but it sits at midnight, so they show as 00:00. If that bothers you, check e.isAllDayEvent() in formatEvent and label them separately.
  • Open-Meteo is generous but not unlimited. One call a day is well within its free tier; don’t loop the briefing or call it on a tight trigger.
  • The task filter treats anything not exactly done as open — including blanks and typos like Done or complete. Keep the status column tidy, or normalise the value with .toLowerCase() before comparing.

Related