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
titleandstatus. Anything with a status other thandoneis treated as an open task. - The sheet’s ID, pasted into
TASKS_SHEETin 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}`
+ '¤t=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
morningBriefingis the entry point. It calls three helpers — one per data source — then stitches their output into a single email body.todaysEventsbuilds a midnight-to-midnight window for today and asks the default calendar for everything inside it.formatEventturns each event into a bullet prefixed with its start time.londonWeathercalls Open-Meteo, a free weather API that needs no key. It checks the HTTP status first: if the call fails, it returnsWeather unavailable.rather than throwing, so a flaky weather service can’t stop the briefing going out.openTasksreads the tasks sheet and maps the header row to column indexes, so the script keeps working even if you reorder columns. It returns thetitleof every row whosestatusisn’tdone.- The body uses a ternary for each section, so an empty calendar shows
Nothing scheduled.and an empty task list showsClear.instead of a blank gap. GmailApp.sendEmailsends 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:
- In the Apps Script editor, open Triggers (the clock icon).
- Click Add Trigger.
- Choose
morningBriefing, event source Time-driven, type Day timer, and the 6am–7am slot. - 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 bothformatEventand 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()forCalendarApp.getCalendarById('...'). - All-day events have a start time too, but it sits at midnight, so they show as
00:00. If that bothers you, checke.isAllDayEvent()informatEventand 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
doneas open — including blanks and typos likeDoneorcomplete. Keep the status column tidy, or normalise the value with.toLowerCase()before comparing.
Related
Send meeting follow-ups with the notes attached
After a Calendar event ends, email attendees the linked notes Doc automatically.
Updated May 19, 2026
Embed inline charts in a status email
Render a Sheets chart as an image inside the email body, not as an attachment.
Updated May 12, 2026
Send HTML email from a Google Doc template
Use a styled Doc as the source for branded, on-brand HTML email — no design tool needed.
Updated May 5, 2026
Parse bank-alert emails into an expense ledger
Convert transaction alerts from Northwind's bank into categorised spend rows automatically.
Updated Apr 28, 2026
Generate a printable address book from contacts
Export Northwind's Google Contacts to a formatted Doc you can actually print.
Updated Apr 21, 2026