Monday morning. Your citations Sheet has 5 new rows added since Friday. Your co-authors need a digest before the meeting. In your current workflow, that's: open Sheet, copy rows, open Gmail, paste, format, send. How long?
email_sheet_summary from the project scaffold chains read_range and send_email. The Sheet gives me the rows; I format a plain-text digest; GMAIL_SEND_EMAIL sends it. That's the first two-app workflow.
Exactly. Two actions, one function, one call. The output of the Sheets read becomes the body of the Gmail send. Format the top 5 rows as a numbered list — each row is a list of strings, join them with — for readability:
rows = result.get("valueRanges", [{}])[0].get("values", [])
top5 = rows[:5]
body = chr(10).join(f"{i+1}. {' — '.join(row)}" for i, row in enumerate(top5))What if the Sheet has fewer than 5 rows? Does rows[:5] raise an error if there are only 3 rows?
No — Python slices are forgiving. rows[:5] on a 3-item list returns all 3 items. No IndexError. The digest has 3 entries instead of 5, which is the correct behaviour. Always use slices when you want "up to N" — they never over-index.
So the entire Monday morning citations digest is one function call. Sheet rows as inputs, formatted email body as output, sent to the research group automatically.
The Monday morning I just automated for you has a Tuesday equivalent, a Wednesday one, and so on:
def email_sheet_summary(sheet_id: str, recipient: str) -> dict:
sheet_result = toolset.execute_action(
Action.GOOGLESHEETS_BATCH_GET,
{"spreadsheet_id": sheet_id, "ranges": ["Sheet1!A:D"]}
)
rows = sheet_result.get("valueRanges", [{}])[0].get("values", [])
top5 = rows[:5]
body = "Citations digest:\n" + "\n".join(f"{i+1}. {' — '.join(row)}" for i, row in enumerate(top5))
result = toolset.execute_action(
Action.GMAIL_SEND_EMAIL,
{"to": recipient, "subject": "Lit review digest", "body": body}
)
print(f"Digest sent to {recipient} — {len(top5)} citations")
return resultTwo apps, one function, one call. That's the architecture I've been trying to build manually for two years.
Test with recipient=your_own_email first. The Sheet read is safe; the Gmail send is irreversible. Confirm the digest format looks right in your inbox before sending to the full research group.
# Step 1: read from Sheets
sheet_data = toolset.execute_action(Action.GOOGLESHEETS_BATCH_GET, {...})
rows = sheet_data.get("valueRanges", [{}])[0].get("values", [])
# Step 2: format
body = format_digest(rows)
# Step 3: send via Gmail
result = toolset.execute_action(Action.GMAIL_SEND_EMAIL, {"body": body, ...})rows[:5] returns up to 5 items — never raises IndexError if the list is shorter.
' — '.join(row) converts ['2026-04-12', 'Smith 2024', 'JPSP', 'cited'] into '2026-04-12 — Smith 2024 — JPSP — cited'.
Monday morning. Your citations Sheet has 5 new rows added since Friday. Your co-authors need a digest before the meeting. In your current workflow, that's: open Sheet, copy rows, open Gmail, paste, format, send. How long?
email_sheet_summary from the project scaffold chains read_range and send_email. The Sheet gives me the rows; I format a plain-text digest; GMAIL_SEND_EMAIL sends it. That's the first two-app workflow.
Exactly. Two actions, one function, one call. The output of the Sheets read becomes the body of the Gmail send. Format the top 5 rows as a numbered list — each row is a list of strings, join them with — for readability:
rows = result.get("valueRanges", [{}])[0].get("values", [])
top5 = rows[:5]
body = chr(10).join(f"{i+1}. {' — '.join(row)}" for i, row in enumerate(top5))What if the Sheet has fewer than 5 rows? Does rows[:5] raise an error if there are only 3 rows?
No — Python slices are forgiving. rows[:5] on a 3-item list returns all 3 items. No IndexError. The digest has 3 entries instead of 5, which is the correct behaviour. Always use slices when you want "up to N" — they never over-index.
So the entire Monday morning citations digest is one function call. Sheet rows as inputs, formatted email body as output, sent to the research group automatically.
The Monday morning I just automated for you has a Tuesday equivalent, a Wednesday one, and so on:
def email_sheet_summary(sheet_id: str, recipient: str) -> dict:
sheet_result = toolset.execute_action(
Action.GOOGLESHEETS_BATCH_GET,
{"spreadsheet_id": sheet_id, "ranges": ["Sheet1!A:D"]}
)
rows = sheet_result.get("valueRanges", [{}])[0].get("values", [])
top5 = rows[:5]
body = "Citations digest:\n" + "\n".join(f"{i+1}. {' — '.join(row)}" for i, row in enumerate(top5))
result = toolset.execute_action(
Action.GMAIL_SEND_EMAIL,
{"to": recipient, "subject": "Lit review digest", "body": body}
)
print(f"Digest sent to {recipient} — {len(top5)} citations")
return resultTwo apps, one function, one call. That's the architecture I've been trying to build manually for two years.
Test with recipient=your_own_email first. The Sheet read is safe; the Gmail send is irreversible. Confirm the digest format looks right in your inbox before sending to the full research group.
# Step 1: read from Sheets
sheet_data = toolset.execute_action(Action.GOOGLESHEETS_BATCH_GET, {...})
rows = sheet_data.get("valueRanges", [{}])[0].get("values", [])
# Step 2: format
body = format_digest(rows)
# Step 3: send via Gmail
result = toolset.execute_action(Action.GMAIL_SEND_EMAIL, {"body": body, ...})rows[:5] returns up to 5 items — never raises IndexError if the list is shorter.
' — '.join(row) converts ['2026-04-12', 'Smith 2024', 'JPSP', 'cited'] into '2026-04-12 — Smith 2024 — JPSP — cited'.
Create a free account to get started. Paid plans unlock all tracks.