眠っている間にTodoistのタスクをコーディングするGitHub Actionを作った

Dev.to / 2026/4/30

💬 オピニオンDeveloper Stack & InfrastructureTools & Practical Usage

要点

  • 著者は、個人のコーディングを自動化するために、小さなTodoistタスクを「Agent_Queue」プロジェクトへ移し、GitHub Actionが5時間ごとにそれを処理するようにしている。
  • ワークフローは、最優先のTodoistタスクを取得し、それをClaude Codeに送って実装を行い、開発ブランチに変更をコミットした後、Todoist上でタスクを完了としてマークする。
  • 5時間ごとの実行間隔は、Claude Code APIトークンの利用量やレート制限を管理するため、また「暴走」する可能性のある自律的な挙動を1回の実行につき1タスクに抑えるために選ばれている。
  • 著者は、朝に出てくるコード出力が強力な出発点になっていることが多い一方で、人間による微調整やレビューがまだ必要になる場合があると報告している。
  • この仕組みは、安全性を「実行頻度(cadence)」で担保することを重視している。実行を間隔を空けることで、何か問題が起きた場合に複数の自律コミットが積み重なるリスクを減らせる。

私は、私のTodoistを読み取り、タスクをコード化して、完了としてマークするGitHub Actionを作った——5時間ごとに

はい、5時間です。いいえ、ランダムではありません。読み進めてください。

自分ひとりで開発しているときの問題点

Todoistのボードが完璧に整理されていて、コーヒーも用意できているのに、コードエディタを開く気力がゼロになる——そんな感覚、分かりますよね?

同じです。

私はフロントエンドエンジニアとして、自分ひとりでプロダクトを作っています。日々の仕事、コンテンツ制作、そしてジムに行くフリをしながらも集中できる時間を見つけてバックログを少しずつ削るのは……なかなか大変です。

そこで、合理的な開発者なら誰でもやることをしました。つまり、問題そのものを自動化で消したのです。

アイデア:エージェント用のキュー

Agent_QueueというTodoistプロジェクトを作りました。小さなコーディングタスク——リファクタリング、新しいコンポーネント、バグ修正——が発生するたびに、AIが理解できるだけの十分に詳しい説明を付けてタスクとして追加します。

次にGitHub Actionが起動し、優先度が最も高いタスクを取り出してClaude Codeに渡します。Claude Codeにやるべきことをやらせて、その結果を私のdevブランチにコミットし、Todoist上でそのタスクを完了としてマークします。

私は朝起きるとコードが書かれています。うまくいくときもあれば、調整が必要なときもあります。でも、自分で作らなくてよい「スタート地点」ができたのが大きいです。

なぜ5時間ごと?

では物語です。

Claude Codeは内部でAPIトークンを使います。これらのトークンには利用上限があり、タスクに対してClaude Codeを自律的に動かしていると……かなり積極的になり得ます。「なぜなら、全コードベースを書き直してしまおう」みたいなノリです。

それを5時間ごとに実行すると、トークン利用の時間を確保して、実行の間に呼吸してリセットできます。さらに、何かがうまくいかない場合(Claudeが暴走する、混沌をコミットする、APIが一時的に不調になるなど)でも、対処すべき被害は1つのタスク分だけで済みます——徹夜で発生した47件もの自律コミットの山を相手にすることにはなりません。

たとえばこう考えてください:1タスク、1セッション、1回の息。5時間の間隔は、Claudeが「取っているはずの休憩時間」を知らないまま用意してくれるコーヒーブレイクです。

また、Anthropicのレート制限は本物です。守りましょう。財布も喜びます。

完全なワークフロー

以下が、仕組みの全手順です:

1. トリガー

on:
  schedule:
    - cron: '0 */5 * * *'  # every 5 hours
  workflow_dispatch:         # or manually, when you're impatient

workflow_dispatchは緊急時の上書きです。Todoistで何か直したときに、「とにかく今すぐ実行したい」ことがあります。私はこれを常に使っています。

2. プロジェクト + 最上位タスクを見つける

アクションがTodoist APIにアクセスし、名前でAgent_Queueプロジェクトを見つけ、すべてのタスクを取得して優先度でソートします。TodoistのAPIではP1 = priority 4です(はい、逆です。聞かないでください。そういう仕様です)。

- name: Fetch top priority task from Todoist
  id: todoist
  env:
    TODOIST_API_KEY: ${{ secrets.TODOIST_API_KEY }}
    TODOIST_PROJECT_NAME: 'Agent_Queue'
  run: |
    PROJECTS=$(curl -s \
      -H "Authorization: Bearer $TODOIST_API_KEY" \
      https://api.todoist.com/api/v1/projects)

    PROJECT_ID=$(echo "$PROJECTS" | python3 -c "
    import json, sys, os
    data = json.load(sys.stdin)
    name = os.environ['TODOIST_PROJECT_NAME']
    match = next((p for p in data if p['name'] == name), None)
    print(match['id'] if match else '')
    ")

JSONのパースにはインラインPythonを使っています。余計な依存関係はなく、セットアップも不要です。ちょっと呪術っぽいですが、ちゃんと美しく動きます。

3. Claude Codeを実行

ここが魔法のステップです。Claude Codeはターミナルで動き、コードベース全体を読み取り、タスクのプロンプトを自律的に実行します。

- name: Install Claude Code
  run: npm install -g @anthropic-ai/claude-code

- name: Run Claude Code on task
  env:
    ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
    TASK_PROMPT: ${{ steps.todoist.outputs.task_prompt }}
  run: |
    echo "$TASK_PROMPT" | claude -p \
      --dangerously-skip-permissions \
      --max-turns 10 \
      --output-format stream-json \
      --verbose 2>&1 | tee claude-output.log

挙げておくべきフラグがいくつかあります:

  • --dangerously-skip-permissions — Claude Codeは通常、何かを実行する前に確認を求めます。CIには確認してくれる人がいないので、スキップします。これは敬意を持って使ってください。
  • --max-turns 10 — Claudeが行う往復の回数にハードな上限を設定します。無限ループを防ぎます。
  • --output-format stream-json — 何が起きたかを実際にデバッグできるよう、すべてをJSONストリームとしてログ出力します。

4. devにコミット

- name: Commit any changes Claude made
  run: |
    git config user.name "Claude Agent"
    git config user.email "claude-agent@users.noreply.github.com"
    git add -A
    if git diff --staged --quiet; then
      echo "No file changes to commit."
    else
      git commit -m "chore: agent task - ${{ steps.todoist.outputs.task_content }}"
      git push
    fi

Claudeがファイルの変更を何もしていない場合(タスクがすでに完了していた、あるいは読み取り専用のタスクだった等)、ワークフローは問題なく終了します。空コミットはありません。

5. Todoistでタスクを完了としてマーク

返却形式: {"translated": "翻訳されたHTML"}
- name: Todoist でタスクを完了としてマークする
  if: steps.todoist.outputs.task_found == 'true' && success()
  env:
    TODOIST_API_KEY: ${{ secrets.TODOIST_API_KEY }}
    TASK_ID: ${{ steps.todoist.outputs.task_id }}
  run: |
    curl -s -X POST \
      -H "Authorization: Bearer $TODOIST_API_KEY" \
      -H "Content-Type: application/json" \
      "https://api.todoist.com/api/v1/tasks/$TASK_ID/close"

success() の条件は重要です — それ以前のすべてが成功した場合にのみ、タスクが完了としてマークされます。Claude がクラッシュした、またはプッシュに失敗した場合、タスクは開いたままで、次のサイクルで再びピックアップされます。

GitHub Secrets You Need

リポジトリへ移動 → SettingsSecrets and variablesActions で、以下を追加します:

Secret 入手先
TODOIST_API_KEY Todoist → Settings → Integrations → Developer
ANTHROPIC_API_KEY console.anthropic.com

他のタスクツールでも動きますか?

はい — そしてここが私の大好きなところです。GitHub Actions の構造は完全に汎用的です。必要なのは、次のことができる REST API を持つタスクツールだけです:

  1. タスクを一覧表示(優先度や並び順などを含む)
  2. ID によってタスクをクローズ/完了する

つまり、Todoist を次のように差し替えられます:

  • Linear — チーム向けで、API もとても良いです
  • Jira — エンタープライズ向けで、より冗長ですが動きます
  • Asana — 同じコンセプトで、エンドポイントが異なります
  • Notion — データベースをタスクリストとして扱えるので、完全に可能
  • GitHub Issues — メタ要素が極まっていますが、動きます

変更するのは、fetch と close のステップで使う curl コマンドだけです。それ以外 — Claude Code のランナー、git のコミット、cron のスケジュール — はまったく同じままです。

しばらく運用してみてのヒント

詳細なタスク説明を書く。 タスクの内容が Claude Code のプロンプトになります。「バグを直して」ではどうにもなりません。「ユーザーが空のメールを送信するとログインページで TypeError が発生する — AuthForm.tsx の null チェックを修正して」が、ちゃんと動く修正につながります。

タイトルだけでなく、タスク説明欄を使う。 私のワークフローでは、存在する場合は説明文を完全なプロンプトとして使い、なければタイトルを使います。ファイル名、期待される挙動、これまでに試したことなど、Claude に文脈を渡してください。

小さなタスクから始める。 初日から「認証モジュール全体をリファクタして」と投げないでください。まずは「LoginForm.tsx の送信ボタンに不足している aria-label を追加して」から始めましょう。エージェントに王国の鍵を渡す前に、信頼を積み上げます。

マージ前に確認する。 これは main ではなく dev にコミットします。マージする前に Claude が出したものを必ずレビューしてください。だいたいは良い、時にはとても良い、たまに理解不能なことがあります。

その雰囲気

私はこれを作りました。最高のツールは、あなたが寝ている間でも動いてくれるものだと信じているからです。「ハスル文化」みたいな話ではなくて、「コンピュータが反復作業をやってくれるので、あなたの頭がやらなくて済む」という意味です。

私の Todoist ボードは、今ではチームのスタンドアップにもなっています。私はタスクを書く。エージェントがそれを拾って実行し、朝座ったときには、私がレビューするためのコミットが待っています。ちょっと変で、でも楽しいワークフローで、私はこれに全力で賛同しています。

完全な YAML

完成したワークフローのファイルは以下です。これを .github/workflows/todoist-sync.yml にドロップすれば完了です。

name: Agent Queue Runner

on:
  schedule:
    - cron: '0 */5 * * *'
  workflow_dispatch:

jobs:
  run-agent-task:
    runs-on: ubuntu-latest
    permissions:
      contents: write

    steps:
      - name: Checkout repo
        uses: actions/checkout@v4
        with:
          ref: dev

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '24'

      - name: Fetch top priority task from Todoist
        id: todoist
        env:
          TODOIST_API_KEY: ${{ secrets.TODOIST_API_KEY }}
          TODOIST_PROJECT_NAME: 'Agent_Queue'
        run: |
          PROJECTS=$(curl -s \
            -H "Authorization: Bearer $TODOIST_API_KEY" \
            -H "Content-Type: application/json" \
            https://api.todoist.com/api/v1/projects)

          if [ -z "$PROJECTS" ]; then
            echo "Todoist API からの空のレスポンスです。"
            exit 1
          fi

返却形式: {"translated": "翻訳されたHTML"}PROJECT_ID=$(echo "$PROJECTS" | python3 -c "
          import json, sys, os
          data = json.load(sys.stdin)
          projects = data.get('results', data) if isinstance(data, dict) else data
          name = os.environ['TODOIST_PROJECT_NAME']
          match = next((p for p in projects if p['name'] == name), None)
          print(match['id'] if match else '')
          ")

          if [ -z "$PROJECT_ID" ]; then
            echo "Project not found."
            echo "task_found=false" >> $GITHUB_OUTPUT
            exit 0
          fi

          TASKS=$(curl -s \
            -H "Authorization: Bearer $TODOIST_API_KEY" \
            -H "Content-Type: application/json" \
            "https://api.todoist.com/api/v1/tasks?project_id=$PROJECT_ID")

          TASK_JSON=$(echo "$TASKS" | python3 -c "
          import json, sys
          data = json.load(sys.stdin)
          tasks = data.get('results', data) if isinstance(data, dict) else data
          if not tasks:
              print('')
              exit()
          tasks.sort(key=lambda t: t.get('priority', 1), reverse=True)
          t = tasks[0]
          prompt = t.get('description', '').strip() or t['content']
          print(json.dumps({'id': t['id'], 'content': t['content'], 'prompt': prompt}))
          ")

          if [ -z "$TASK_JSON" ]; then
            echo "No tasks found."
            echo "task_found=false" >> $GITHUB_OUTPUT
            exit 0
          fi

          TASK_ID=$(echo "$TASK_JSON" | python3 -c "import json,sys; print(json.load(sys.stdin)['id'])")
          TASK_CONTENT=$(echo "$TASK_JSON" | python3 -c "import json,sys; print(json.load(sys.stdin)['content'])")
          TASK_PROMPT=$(echo "$TASK_JSON" | python3 -c "import json,sys; print(json.load(sys.stdin)['prompt'])")

          echo "task_found=true" >> $GITHUB_OUTPUT
          echo "task_id=$TASK_ID" >> $GITHUB_OUTPUT
          echo "task_content<<EOF" >> $GITHUB_OUTPUT
          echo "$TASK_CONTENT" >> $GITHUB_OUTPUT
          echo "EOF" >> $GITHUB_OUTPUT
          echo "task_prompt<<EOF" >> $GITHUB_OUTPUT
          echo "$TASK_PROMPT" >> $GITHUB_OUTPUT
          echo "EOF" >> $GITHUB_OUTPUT

      - name: Claude Codeをインストール
        if: steps.todoist.outputs.task_found == 'true'
        run: npm install -g @anthropic-ai/claude-code

      - name: タスクに対してClaude Codeを実行
        if: steps.todoist.outputs.task_found == 'true'
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
          TASK_PROMPT: ${{ steps.todoist.outputs.task_prompt }}
        run: |
          echo "$TASK_PROMPT" | claude -p \
            --dangerously-skip-permissions \
            --max-turns 10 \
            --output-format stream-json \
            --verbose 2>&1 | tee claude-output.log

      - name: Claudeが行った変更をコミットする
        if: steps.todoist.outputs.task_found == 'true'
        run: |
          git config user.name "Claude Agent"
          git config user.email "claude-agent@users.noreply.github.com"
          git add -A
          if git diff --staged --quiet; then
            echo "No changes to commit."
          else
            git commit -m "chore: agent task - ${{ steps.todoist.outputs.task_content }}"
            git push
          fi

      - name: Todoistでタスクを完了としてマークする
        if: steps.todoist.outputs.task_found == 'true' && success()
        env:
          TODOIST_API_KEY: ${{ secrets.TODOIST_API_KEY }}
          TASK_ID: ${{ steps.todoist.outputs.task_id }}
        run: |
          curl -s -X POST \
            -H "Authorization: Bearer $TODOIST_API_KEY" \
            -H "Content-Type: application/json" \
            "https://api.todoist.com/api/v1/tasks/$TASK_ID/close"
          echo "Task marked complete."

これを作成するか、Linear/Jira/Notion向けに改造する場合は、コメントで教えてください。どんなバリエーションが出てくるかぜひ見てみたいです。

この手の内容をもっと見るために、DEVでは@eli_coding、Instagramでは@eli_codingをフォローしてください。