Five weeks of actions. Three apps. One function. Walk me through the capstone logic before writing it.
First, read the progress rows from Sheets. Then draft an advisor email summarising the latest row. Then create a Calendar event for the next check-in. Return a dict with all three results.
Perfect sequencing. read_range gives the rows. The last row is the most recent progress entry — format its fields into the email body. draft_email creates the draft. Then create_event books the next session. All three results go into a summary dict:
def run_workflow(sheet_id: str, advisor_email: str, cal_id: str) -> dict:
rows = read_range(sheet_id, "Sheet1!A:C")
last_row = rows[-1] if rows else ["No data", "", ""]
body = f"Latest entry: {' | '.join(last_row)}"
draft = draft_email(advisor_email, "Research progress update", body)
event = create_event(cal_id, "Advisor check-in", "2026-04-27T10:00:00Z", "2026-04-27T10:30:00Z")
return {"rows_read": len(rows), "draft": draft, "event": event}What if rows is empty? rows[-1] would crash.
Guard it — exactly the defensive pattern from Day 24. Check if rows before indexing. And the return dict tells the caller exactly what happened at each step:
def run_workflow(sheet_id: str, advisor_email: str, cal_id: str) -> dict:
rows = read_range(sheet_id, "Sheet1!A:C")
last_row = rows[-1] if rows else []
body = f"Latest entry: {' | '.join(last_row)}" if last_row else "No progress rows found."
draft = draft_email(advisor_email, "Research progress update", body)
event = create_event(cal_id, "Advisor check-in", "2026-04-27T10:00:00Z", "2026-04-27T10:30:00Z")
print(f"Workflow complete: {len(rows)} rows, draft created, event booked")
return {"rows_read": len(rows), "draft": draft, "event": event}One call. Three apps. The entire Friday ritual automated. I wrote each piece across five weeks — this is just putting them together.
That's exactly the point. The capstone is not a new skill — it's the proof that each individual function you wrote is composable. Every action in your toolkit is a Lego brick. run_workflow is the assembly.
Five weeks of functions assembled into one call. The hard part was always the individual actions — now they're all just helper calls.
That's the design principle. Each function is small, testable, and composable. Complex workflows are just careful sequencing of simple, correct functions. Write small — compose big.
read_range(sheet_id, range) → get all progress rowsdraft_email(to, subject, body) → create Gmail draft from last rowcreate_event(cal_id, title, start, end) → book Calendar check-in{"rows_read": int, "draft": dict, "event": dict} — one key per workflow step, so the caller can inspect each result independently.
last_row = rows[-1] if rows else [] — never index an empty list. Use a ternary to build the body string only when data exists.
Each sub-function (read_range, draft_email, create_event) was tested independently in earlier lessons. The capstone assumes they work and focuses only on sequencing and error guards.
Five weeks of actions. Three apps. One function. Walk me through the capstone logic before writing it.
First, read the progress rows from Sheets. Then draft an advisor email summarising the latest row. Then create a Calendar event for the next check-in. Return a dict with all three results.
Perfect sequencing. read_range gives the rows. The last row is the most recent progress entry — format its fields into the email body. draft_email creates the draft. Then create_event books the next session. All three results go into a summary dict:
def run_workflow(sheet_id: str, advisor_email: str, cal_id: str) -> dict:
rows = read_range(sheet_id, "Sheet1!A:C")
last_row = rows[-1] if rows else ["No data", "", ""]
body = f"Latest entry: {' | '.join(last_row)}"
draft = draft_email(advisor_email, "Research progress update", body)
event = create_event(cal_id, "Advisor check-in", "2026-04-27T10:00:00Z", "2026-04-27T10:30:00Z")
return {"rows_read": len(rows), "draft": draft, "event": event}What if rows is empty? rows[-1] would crash.
Guard it — exactly the defensive pattern from Day 24. Check if rows before indexing. And the return dict tells the caller exactly what happened at each step:
def run_workflow(sheet_id: str, advisor_email: str, cal_id: str) -> dict:
rows = read_range(sheet_id, "Sheet1!A:C")
last_row = rows[-1] if rows else []
body = f"Latest entry: {' | '.join(last_row)}" if last_row else "No progress rows found."
draft = draft_email(advisor_email, "Research progress update", body)
event = create_event(cal_id, "Advisor check-in", "2026-04-27T10:00:00Z", "2026-04-27T10:30:00Z")
print(f"Workflow complete: {len(rows)} rows, draft created, event booked")
return {"rows_read": len(rows), "draft": draft, "event": event}One call. Three apps. The entire Friday ritual automated. I wrote each piece across five weeks — this is just putting them together.
That's exactly the point. The capstone is not a new skill — it's the proof that each individual function you wrote is composable. Every action in your toolkit is a Lego brick. run_workflow is the assembly.
Five weeks of functions assembled into one call. The hard part was always the individual actions — now they're all just helper calls.
That's the design principle. Each function is small, testable, and composable. Complex workflows are just careful sequencing of simple, correct functions. Write small — compose big.
read_range(sheet_id, range) → get all progress rowsdraft_email(to, subject, body) → create Gmail draft from last rowcreate_event(cal_id, title, start, end) → book Calendar check-in{"rows_read": int, "draft": dict, "event": dict} — one key per workflow step, so the caller can inspect each result independently.
last_row = rows[-1] if rows else [] — never index an empty list. Use a ternary to build the body string only when data exists.
Each sub-function (read_range, draft_email, create_event) was tested independently in earlier lessons. The capstone assumes they work and focuses only on sequencing and error guards.
Create a free account to get started. Paid plans unlock all tracks.