I tried to semi-automate daily report creation using Google Calendar API and Amazon Bedrock
This page has been translated by machine translation. View original
Introduction
Writing daily reports is time-consuming. Most of the content consists of what you did that day and plans for tomorrow. This information is already in Google Calendar. I was originally manually pasting JSON obtained from the Google Calendar API into Claude's prompt to create draft reports, but wanting to streamline this into a single CLI command, I created a semi-automated tool.
This article introduces the system's structure and implementation, particularly focusing on the Lambda + Bedrock components.
What is Amazon Bedrock
Amazon Bedrock is an AWS fully managed service that allows you to use foundation models provided by companies like Anthropic and Meta via API. AWS manages model hosting and scaling, allowing application developers to focus on calling inferences.
Testing Environment
- Node.js 22
- TypeScript 5.7
- AWS Lambda (nodejs22.x)
- Amazon Bedrock (Claude Sonnet 4.5)
- Terraform 1.5
Target Audience
- Engineers looking to streamline daily report or report creation
- Those interested in practical use cases of Google Calendar API
- Those interested in application development using Amazon Bedrock
References
System Overview
The configuration is as follows:
The local CLI retrieves schedules from Google Calendar, filters out unnecessary events, and POSTs them to API Gateway. Lambda calls Bedrock (Claude Sonnet 4.5) and returns the generated daily report Markdown.
The reasons for separating the CLI and AWS are mainly the following two points:
- Wanted to keep Google OAuth token management local
- By delegating LLM calls to Lambda, prompt or model changes can be reflected without rebuilding the CLI
Technology Stack
- CLI: TypeScript (Node.js 22), googleapis, @smithy/signature-v4
- Lambda: TypeScript, @aws-sdk/client-bedrock-runtime
- Infrastructure: Terraform (API Gateway HTTP API, Lambda, IAM)
Retrieving and Filtering Events from Google Calendar
The CLI uses Google Calendar API v3's events.list to retrieve schedules for the target day and the next day based on JST. Not all retrieved schedules are passed to the LLM. The CLI performs the following filtering:
- Excluding canceled events
- Excluding all-day events and multi-day events
- Excluding events with empty titles
- Excluding events with titles that exactly match those specified in the configuration file (
config.yaml) - Filtering by participation status
The participation status determination logic is implemented as follows:
function computeMyStatus(
event: calendar_v3.Schema$Event
): "OWNER" | "YES" | "NO" {
if (event.organizer?.self === true || event.creator?.self === true) {
return "OWNER";
}
const selfAttendee = event.attendees?.find((a) => a.self === true);
if (selfAttendee?.responseStatus === "accepted") {
return "YES";
}
return "NO";
}
Only events where you are the organizer (OWNER) or have accepted the invitation (YES) are sent to the LLM. Declined or unanswered events don't need to be included in the report. The purpose of filtering on the CLI side is to reduce token consumption by not sending unnecessary data to the LLM.
Daily Report Generation with Amazon Bedrock
This is the core of the system. Lambda builds a prompt based on the data received from the CLI and calls Claude Sonnet 4.5 using Bedrock's InvokeModel API.
Lambda Input and Output
The input from CLI to Lambda has the following JSON structure:
{
"date": "2026-02-14",
"timezone": "Asia/Tokyo",
"calendar_today": [
{ "date": "2026-02-14", "start": "10:00", "end": "11:00", "title": "Team Meeting", "myStatus": "OWNER" }
],
"calendar_tomorrow": [...],
"prev_report_markdown": "Previous business day's report Markdown (if available)",
"hitl_markdown": "User's free-form memo"
}
Lambda constructs a prompt from this input, calls Bedrock, and returns the generated Markdown as is.
Calling Bedrock InvokeModel
The Bedrock call part is as follows:
const MODEL_ID = "jp.anthropic.claude-sonnet-4-5-20250929-v1:0";
const client = new BedrockRuntimeClient({});
const command = new InvokeModelCommand({
modelId: MODEL_ID,
contentType: "application/json",
accept: "application/json",
body: JSON.stringify({
anthropic_version: "bedrock-2023-05-31",
max_tokens: 2048,
system: systemPrompt,
messages: [{ role: "user", content: userMessage }],
}),
});
const response = await client.send(command);
const responseBody = JSON.parse(new TextDecoder().decode(response.body));
const markdown = responseBody.content?.[0]?.text;
The jp. prefix in the model ID is because we're using a cross-region inference profile. Even when requesting from ap-northeast-1, AWS automatically routes to available regions in Japan (ap-northeast-1, ap-northeast-3, etc.).
resource "aws_iam_role_policy" "lambda_bedrock" {
policy = jsonencode({
Statement = [{
Effect = "Allow"
Action = ["bedrock:InvokeModel"]
Resource = [
"arn:aws:bedrock:*::foundation-model/anthropic.claude-sonnet-4-5-20250929-v1:0",
"arn:aws:bedrock:ap-northeast-1:${account_id}:inference-profile/jp.anthropic.claude-sonnet-4-5-20250929-v1:0"
]
}]
})
}
Prompt Design
To stabilize the output format of the daily report, we explicitly specify the output template and rules in the system prompt.
You are a daily report creation assistant. Using only the provided information, generate a daily report following this Markdown template.
## Output Rules
- Output only the Markdown body. No preface, explanation, or code blocks.
- Fixed headings in the following 5 sections:
1. # Daily Report yyyy/mm/dd (day of week) 9:00-18:00 (break 1:00)
2. ## What I Did
3. ## Tomorrow's Schedule
4. ## Future Tasks
5. ## Comment
- Do not make any unfounded assumptions or fabrications.
- If there is no information, write "- None".
There are two points I particularly focused on:
-
Fixed Heading Structure
To avoid confusing readers with changing report formats, the heading names and order are fixed in the prompt. -
Preventing Fabrication
LLMs tend to generate plausible content, but it's problematic to have things you didn't actually do written in a daily report. By explicitly stating rules like "Do not make any unfounded assumptions or fabrications" and "If there is no information, write none", we constrain the model from generating information not found in the calendar or memo.
API Gateway and SigV4 Authentication
API Gateway HTTP API is placed in front of Lambda. The reason for choosing HTTP API rather than REST API is cost. HTTP API has lower prices compared to REST API and is suitable for simple APIs like this one.
IAM authentication (SigV4) was adopted for authentication. On the CLI side, @smithy/signature-v4 is used to sign requests. The advantages are that API key management is unnecessary and it leverages existing AWS IAM mechanisms.
Infrastructure is configured with Terraform. Resources consist of four types: API Gateway HTTP API, Lambda, IAM roles/policies, and CloudWatch Logs, keeping the configuration minimal.
Execution Result
When actually executed, it looked like this:
$ npm run report
Daily report generation: 2026-02-14
Edit memo file: /path/to/memo/memo-2026-02-14.md
Enter "yes" when editing is complete (anything else to cancel):
>
I added free-form content to memo/memo-2026-02-14.md and entered yes.
> yes
Configuration loaded.
Authenticating with Google Calendar...
Authentication complete.
Retrieving calendar events...
Today: 5 events, Tomorrow: 3 events
After filtering - Today: 4 events, Tomorrow: 2 events
Previous business day's report loaded.
Generating daily report...
Daily report saved: /path/to/daily/daily-report-user-2026-02-14.md
Here's an example of the generated daily report (event names are replaced with dummies):
# 日報 2026年02月14日 (土) 9:00-18:00 (休憩 1:00)
## やったこと
- チーム定例
- プロジェクト A 設計レビュー
- 社内勉強会 (Bedrock ハンズオン)
- 1on1
## 明日の予定
- スプリントプランニング
- プロジェクト B キックオフ
## 今後のタスク
- プロジェクト A テスト環境構築
- ドキュメント更新
## ひとこと
勉強会で Bedrock の活用事例を共有できた。
Conclusion
I created a system that generates daily report drafts with a single command by combining Google Calendar API and Amazon Bedrock. It has a simple configuration that filters calendar events, passes them to the LLM, and generates Markdown according to a fixed template. By adopting a serverless configuration with API Gateway + Lambda, operational costs are also minimized.
In the future, I'd like to explore ways to utilize daily report data, such as accumulating generated reports as material for end-of-term reports.