Back to Blog
GitHub ActionsPythonGoogle SheetsClaude APIAutomationIndie Dev

I Built a Fully Automated Tweet System: Google Sheets + GitHub Actions + Claude API

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 when content/blog/*.md changes, runs Claude API, writes to sheet
  • tweet.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 skip in the status column
  • Full history at a glancedone status 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/.

Share:
View all posts