I Built a Fully Automated Tweet System: Google Sheets + GitHub Actions + Claude API
"I forgot to tweet again today."
I launched SheetToolBox and knew I needed to promote it consistently. But between a day job and active development, posting to X kept falling to the bottom of the list.
The problem wasn't motivation. It was the daily cognitive load: what do I even post about today? That friction, compounded, kills consistency.
So I automated both the thinking and the execution.
What I Built
A system where publishing a blog post automatically generates tweet drafts and queues them for posting three times a day.
New blog post pushed to GitHub
↓ GitHub Actions (generate-tweets.yml)
↓ Claude API generates 5 tweet drafts
Google Sheets (posting queue)
↓ GitHub Actions cron (3x daily)
Auto-posted to X
The two workflows run independently:
generate-tweets.yml— triggers on push whencontent/blog/*.mdchanges, runs Claude API, writes to sheettweet.yml— runs on cron at 8:00, 12:00, 19:00 JST, reads one row from the sheet and posts it
After I publish a post, the queue automatically refills. I don't touch anything else.
Why These Technical Choices
Google Sheets as the Queue
I considered a database and JSON files, but a spreadsheet won:
- Manually editable — want to skip a tweet? Write
skipin the status column - Full history at a glance —
donestatus and timestamp recorded automatically - Zero infrastructure — no extra server, no database to manage
| Column A (tweet text) | Column B (status) | Column C (posted at) |
|---|---|---|
| SheetToolBox is live! | done | 2026-02-21 08:00:12 |
| Cut your Excel work in half. |
Any row with an empty status column is the next candidate. done or skip gets passed over. Dead simple.
GitHub Actions for Scheduling
| Option | Cost | Complexity |
|---|---|---|
| GitHub Actions | Free (public repos) | 1 yml file |
| Cloud Run Jobs | Pay-per-use | Moderate |
| Always-on VPS | ~$5+/month | Server management |
I already use GitHub for Vercel deploys. Actions was the obvious choice. Two cron jobs, each under 50 lines of yml.
Generating Tweet Drafts with Claude API
The key insight: generating ideas is the hard part, not the posting.
generate_tweets.py reads a Markdown blog post, sends the first 3,000 characters to Claude claude-opus-4-6, and appends 5 tweet drafts to the spreadsheet. I fine-tuned the prompt to match a tone I actually want to post in:
PROMPT_TEMPLATE = """以下のブログ記事から、X(Twitter)への投稿文を{count}件作成してください。
【文体・トーンの指示】
あなたは業務効率化・AI活用を発信するインディーメーカーです。
- ですます調、語りかけるトーン(「〜だと実感しています」「〜が大切だと感じています」)
- 構造: ①主張・気づき → ②理由 → ③具体例 → ④まとめ・行動提案
- 絵文字は最後に1つだけ(任意)
- ハッシュタグは使わない
- 宣伝口調ではなく「学びや気づきを共有する」スタンスで
【制約】
- 各投稿は{max_chars}文字以内(厳守)
- 各投稿を空行1つで区切って出力すること
...
"""
Tone matters more than most people think. A prompt that generates "educational insights" in your own voice is worth refining carefully.
The --dry-run flag lets me preview drafts without touching the spreadsheet:
python sns/auto-tweet/generate_tweets.py content/blog/article.md --dry-run
Automatic Generation on Push
The generate-tweets.yml workflow detects newly added .md files in content/blog/ and runs the script automatically:
on:
push:
branches: [main]
paths:
- "content/blog/**/*.md"
- name: Find new blog posts
id: detect
run: |
NEW_FILES=$(git diff --name-only --diff-filter=A HEAD~1 HEAD -- 'content/blog/*.md')
echo "files=$NEW_FILES" >> $GITHUB_OUTPUT
Only newly added files trigger generation — editing an existing post doesn't flood the queue with duplicates.
The Hardest Part: Google Authentication
For Google Sheets access in GitHub Actions, I used Workload Identity Federation (WIF) instead of a service account JSON key.
Dropping a JSON key into GitHub Secrets works, but:
- JSON keys are long-lived — a leaked key stays valid indefinitely
- WIF generates short-lived tokens at runtime — no key files to manage
Setup is more steps upfront (GCP Workload Identity Pool → OIDC provider → IAM binding), but once done it's maintenance-free. The workflow step itself is minimal:
- uses: google-github-actions/auth@v2
with:
workload_identity_provider: ${{ secrets.WIF_PROVIDER }}
service_account: ${{ secrets.WIF_SERVICE_ACCOUNT }}
create_credentials_file: true
What I Learned After Running It
What works well
- No more missed posts — the biggest win by far
- Queue stays full automatically — every new post adds 5 drafts
- Timing handled without me thinking about it
Things to watch for
- X API Free Tier caps at 1,500 posts/month — at 3/day × 30 days = 90 posts, there's plenty of headroom
- GitHub Actions cron isn't precise — delays of 5–15 minutes are normal under load
Summary
The total code is two Python files (~200 lines) and two yml files.
post_tweet.py reads one row from the spreadsheet and posts to X. generate_tweets.py calls Claude and writes drafts. Keeping those responsibilities separate made both scripts easy to maintain and debug independently.
The infrastructure cost is zero — GitHub Actions and Google Sheets are free at this scale. The only ongoing cost is Claude API usage, roughly a few dollars a month.
The biggest benefit isn't saving time on posting. It's removing the daily question of what do I even say today. That mental overhead was the real bottleneck, and automating it cleared space for writing and building.
Code is in the chiapuru-portfolio repository under sns/auto-tweet/.