Start Debugging

How to Schedule a Recurring Claude Code Task That Triages GitHub Issues

Three ways to put Claude Code on a schedule that triages GitHub issues unattended in 2026: cloud Routines (the new /schedule), the claude-code-action v1 with cron + issues.opened, and the session-scoped /loop. Includes a runnable Routine prompt, a complete GitHub Actions YAML, jitter and identity gotchas, and when to pick which.

A scheduled triage pass over a GitHub backlog is one of the most useful things you can ask a coding agent to do, and it is also the easiest to get wrong. As of April 2026 there are three different “schedule a Claude Code task” primitives, they live in different runtimes, and they have very different failure modes. This post walks through all three for the same job, “every weekday morning at 8am, label and route every new issue in my repo,” using Claude Code v2.1.x, the anthropics/claude-code-action@v1 GitHub Action, and the routines research preview that Anthropic shipped on April 14, 2026. The model is claude-sonnet-4-6 for the triage prompt and claude-opus-4-7 for the dedupe pass.

The short answer: use a cloud Routine with both a schedule trigger and a GitHub issues.opened trigger if your account has Claude Code on the web enabled. Fall back to a GitHub Actions schedule + workflow_dispatch + issues.opened workflow if you need it on Bedrock, Vertex, or your own runners. Use /loop only for ad-hoc polling while a session is open, never for unattended triage.

Why the three options exist, and which to pick

Anthropic deliberately ships three different schedulers because the tradeoffs are real. The official scheduling docs put them on one page:

CapabilityRoutines (cloud)GitHub Actions/loop (session)
Where it runsAnthropic infrastructureGitHub-hosted runnerYour terminal
Survives a closed laptopYesYesNo
Triggered by issue.openedYes (native)Yes (workflow event)No
Local file accessNo (fresh clone)Yes (checkout)Yes (current cwd)
Minimum interval1 hour5 minutes (cron quirk)1 minute
Auto-expiresNoNo7 days
Permission promptsNone (autonomous)None (claude_args)Inherited from session
Plan requirementPro / Max / Team / Ent.Any plan with API keyLocal CLI

For “triage every new issue and run a daily sweep,” the cloud routine is the right primitive. It has a native GitHub trigger so you do not have to wire up actions/checkout, the prompt is editable from the web UI without a PR, and the runs do not consume any of your GitHub Actions minutes. The only reason to skip it is if your org runs Claude through AWS Bedrock or Google Vertex AI, in which case the cloud routines are not yet available and you fall back to the action.

The triage routine, end to end

A routine is “a saved Claude Code configuration: a prompt, one or more repositories, and a set of connectors, packaged once and run automatically.” Every run is an autonomous Claude Code cloud session, with no permission prompts, that clones your repo from the default branch and writes any code changes to a claude/-prefixed branch by default.

Create one from inside any Claude Code session:

# Claude Code 2.1.x
/schedule weekdays at 8am triage new GitHub issues in marius-bughiu/start-debugging

/schedule walks you through the same form the web UI at claude.ai/code/routines shows: name, prompt, repositories, environment, connectors, and triggers. Everything you set on the CLI is editable on the web, and the same routine shows up on Desktop, web, and CLI immediately. One important constraint: /schedule only attaches schedule triggers. To add the issues.opened GitHub trigger that makes triage near-instant, edit the routine on the web after creation.

The prompt

A routine runs with no human in the loop, so the prompt has to be self-contained. The Anthropic team’s own example phrasing in the routines docs is “applies labels, assigns owners based on the area of code referenced, and posts a summary to Slack so the team starts the day with a groomed queue.” Concretely:

# Routine prompt: daily-issue-triage
# Model: claude-sonnet-4-6
# Repos: marius-bughiu/start-debugging

You are the issue triage bot for this repository. Every run, do the following.

1. List every issue opened or updated since the last successful run of this
   routine, using `gh issue list --search "updated:>=YYYY-MM-DD"` with the
   timestamp of the previous run from the routine's session history. If you
   cannot find a previous run, scope to the last 24 hours.

2. For each issue, classify it as exactly one of: bug, feature, docs,
   question, support, spam. Apply that label with `gh issue edit`.

3. Assess priority as one of: p0, p1, p2, p3. Apply that label too.
   p0 only when the issue describes a production-affecting regression
   with a reproducer.

4. Look up the touched code area. Use `gh search code --repo` and `rg`
   against the cloned working copy to find the most likely owner via
   the `CODEOWNERS` file. Assign that user. If there is no CODEOWNERS
   match, leave it unassigned and apply the `needs-triage` label.

5. Run a duplicate check. Use `gh issue list --search "<title keywords>
   in:title is:open"` to find similar open issues. If you find one with
   high confidence, post a comment on the new issue: "This looks like
   a duplicate of #N. Closing in favor of that thread; please reopen
   if I got it wrong." and then `gh issue close`.

6. Post a single Slack message to #engineering-triage via the connector
   summarizing what you did: counts per label, p0 issues by number, and
   any issue that you could not classify with confidence above 0.7.

Do not push any commits. Do not modify code. The only writes this routine
performs are GitHub label/assign/comment/close calls and one Slack message.

Two non-obvious details worth pinning down:

The triggers

In the routine’s edit form, attach two triggers:

  1. Schedule, weekdays at 08:00. Times are in your local zone and converted to UTC server-side, so a US-Pacific schedule and a CET schedule both fire at the same wall-clock time wherever the cloud session lands. Routines add a deterministic stagger of up to a few minutes per account, so do not set the schedule to 0 8 if exact timing matters, set it to :03 or :07.
  2. GitHub event, issues.opened. This makes the routine fire within seconds of every new issue, in addition to the 8am sweep. The two are not redundant: the schedule trigger catches everything that lands while the GitHub App is paused or behind on the per-account hourly cap, and the event trigger keeps fresh issues from sitting cold for a workday.

To attach the issues.opened trigger, the Claude GitHub App has to be installed on the repository. /web-setup from the CLI grants clone access only and does not enable webhook delivery, so installing the app from the web UI is required.

The custom cron expression

The schedule presets are hourly, daily, weekdays, and weekly. For anything else, pick the closest preset in the form, then drop into the CLI:

/schedule update

Walk through the prompts to the schedule section and supply a custom 5-field cron expression. The only hard rule is that the minimum interval is one hour; an expression like */15 * * * * is rejected at save time. If you genuinely need a tighter cadence, that is a signal you want the GitHub Actions path or the event trigger, not the schedule trigger.

The GitHub Actions fallback

If your team is on Bedrock or Vertex, or you simply prefer the audit trail of an Actions run log, the same job runs as a workflow with claude-code-action@v1. The action went GA on August 26, 2025 and the v1 surface is unified around two inputs: a prompt and a claude_args string that passes any flag straight through to the Claude Code CLI. The full upgrade table from the beta surface lives in the GitHub Actions docs.

# .github/workflows/issue-triage.yml
# claude-code-action v1, claude-sonnet-4-6, schedule + issues.opened + manual
name: Issue triage

on:
  schedule:
    - cron: "3 8 * * 1-5"  # weekdays 08:03 UTC, off the :00 boundary
  issues:
    types: [opened]
  workflow_dispatch:        # manual run from the Actions tab

permissions:
  contents: read
  issues: write
  pull-requests: read
  id-token: write

jobs:
  triage:
    runs-on: ubuntu-latest
    timeout-minutes: 10
    steps:
      - uses: actions/checkout@v4

      - uses: anthropics/claude-code-action@v1
        with:
          anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
          prompt: |
            REPO: ${{ github.repository }}
            EVENT: ${{ github.event_name }}
            ISSUE: ${{ github.event.issue.number }}

            On a schedule run, list open issues updated in the last 24 hours
            and triage each one. On an `issues.opened` event, triage only
            the single issue ${{ github.event.issue.number }}.

            For each issue:
            1. Classify as bug / feature / docs / question / support / spam.
            2. Assess priority p0 / p1 / p2 / p3.
            3. Apply both labels with `gh issue edit`.
            4. Resolve the touched area via CODEOWNERS and assign the owner,
               or apply `needs-triage` if no match.
            5. Search for duplicates by title keywords. Comment and close
               only if confidence is high.

            Do not edit code. Do not push. Only GitHub label / assign /
            comment / close calls are allowed.
          claude_args: |
            --model claude-sonnet-4-6
            --max-turns 12
            --allowedTools "Bash(gh:*),Read,Grep"

Three things this workflow gets right that a hand-rolled cron does not. workflow_dispatch alongside schedule puts a “Run workflow” button in the Actions tab so you can test without waiting for 8am. --allowedTools "Bash(gh:*),Read,Grep" uses the same gating as the local CLI; without it, the action would have Edit and Write access too. The :03 minute sidesteps the wide non-deterministic delay GitHub Actions adds to free-tier cron triggers during peak hours. This is essentially the issue triage example from the action’s solutions guide, with a schedule trigger and a tighter tool allowlist.

When /loop is the right primitive

/loop is the third option and it is the one to reach for the least for triage work. The scheduled-tasks docs spell out the constraints:

The right use for /loop is to babysit a triage routine you are still tuning, not to run the triage itself. Inside an open session pointed at the repo:

/loop 30m check the last 5 runs of the daily-issue-triage routine on
claude.ai/code/routines and tell me which ones produced label edits
that look wrong. Skip silently if nothing has changed.

Claude converts 30m to a cron expression, schedules the prompt under a generated 8-character ID, and re-fires it between your turns until you press Esc or seven days elapse. That is genuinely useful for a “is the routine drifting?” feedback loop while a human stays at the keyboard. It is the wrong shape for “run forever, unattended.”

Gotchas worth knowing before the first run

A few things will bite you on the first scheduled run if you do not plan for them.

Identity. Routines belong to your individual claude.ai account, and anything the routine does through your connected GitHub identity appears as you. For an open-source repo, install the routine under a dedicated bot account, or use the GitHub Actions path with a separate Claude GitHub App bot install.

Daily run cap. Routines have a per-plan daily cap (Pro 5, Max 15, Team and Enterprise 25). Each issues.opened event is one run, so a repo that gets 30 issues a day caps out before lunch unless you enable extra usage in billing. The schedule-only routine and the GitHub Actions path both sidestep this; the latter bills against API tokens instead.

Branch-push safety. A routine can only push to claude/-prefixed branches by default. The triage prompt above does not push at all, but extending it to open a fix PR means either accepting the prefix or enabling Allow unrestricted branch pushes per repo. Do not flip that switch absent-mindedly.

The experimental-cc-routine-2026-04-01 beta header. The /fire endpoint that backs the API trigger ships under that header today. Anthropic keeps the two most recent dated versions working when they break, so build the header into a constant and rotate at version flips, not into every webhook.

Stagger and no catch-up. Both runtimes add a deterministic offset (up to 10% of period for routines, much wider for free-tier Actions during peak), and neither replays missed fires. The schedule + issues.opened combo handles the catch-up gap better than schedule alone because the event trigger covers the dead zone.

Routines are still in research preview, so the exact UI and the /fire beta header will move. The model that any of this targets, though, is stable: a self-contained prompt, scoped tool access, deterministic triggers, and an audit trail per run. That is the part to design carefully. The runtime is the part you can swap.

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< Back