Migrating from actions/stale to better-stale-bot#
Overview#
This guide covers migrating from actions/stale to better-stale-bot. The two tools solve the same problem — keeping an issue backlog from going silent forever — but they do it in fundamentally different ways.
actions/stale works by a timer: after a configurable number of days without activity, it applies a label and posts a static template comment. It has no understanding of what the issue is about or whether the problem was resolved.
better-stale-bot runs an AI agent against every inactive thread. Before labeling an issue stale, the agent reads the full thread, writes a 2–4 sentence summary of what the issue is about and where the discussion left off, classifies the thread as RESOLVED or UNRESOLVED, and posts a contextual comment in the language of the issue title . The reasoning is visible in the GitHub Actions run — you can open the Agent Summary on any run and read why each issue was handled the way it was.
Why migrate?
- Contextual comments: Every stale comment explains the specific issue, not a generic "This issue has been automatically marked as stale."
- Resolved/unresolved classification: The agent identifies whether an issue looks resolved even if the author never explicitly closed it .
- Language-aware: Comments are written in the detected language of the issue title — no template localization needed .
- Engagement-based prioritization: Quiet threads are processed first; high-engagement issues are preserved longer .
- Auditable reasoning: Every run logs the full agent conversation, engagement score calculations, and token usage.
⚠️ Before you start — disable
actions/stalefirst. Running both bots simultaneously will produce competing stale labels, duplicate comments on issues, and conflicting label states. Delete or disable youractions/staleworkflow file before enabling better-stale-bot .
Setting Mapping#
Most actions/stale configuration options have a direct or near-direct equivalent in better-stale-bot. The table below maps each option and notes where the equivalent is configured.
actions/stale option | better-stale-bot equivalent | Where to configure |
|---|---|---|
days-before-stale | days-before-stale (default: 60) | ## Configuration table in the Markdown body |
days-before-close | days-before-close (default: 7) | ## Configuration table in the Markdown body |
stale-issue-message | No static template — the AI generates a per-issue summary and comment | Edit the tone and structure instructions in the Markdown body |
close-issue-message | No static template — the closing comment is generated contextually and passed as the body of close-issue | Edit the Markdown body instructions |
stale-issue-label | Hardcoded as Stale (also in safe-outputs allowed list) | ## Configuration in the Markdown body + allowed in frontmatter safe-outputs; recompile after changing the label name |
exempt-issue-labels | Exempt labels list (defaults: agentic-workflows, pinned, security, help wanted) | ## Configuration in the Markdown body; no recompile needed |
operations-per-run | Replaced by per-type safe-outputs caps | YAML frontmatter only; requires gh aw compile after changes |
days-before-pr-stale / days-before-pr-close | Not applicable — the shipped workflow is issues-only | See Pull Request Support |
close-issue-reason | Defaults to NOT_PLANNED | Hardcoded in the workflow instructions |
Note on defaults: better-stale-bot's
days-before-staledefaults to 60 days. If youractions/staleconfiguration used a longer threshold (e.g., 90 days), setdays-before-staleto match your old value on the first run to avoid marking issues stale that were previously within your window.
Safe Outputs vs. operations-per-run#
actions/stale uses a single operations-per-run integer to cap total write operations per run. better-stale-bot replaces this with separate safe-outputs caps in the YAML frontmatter — one per operation type :
safe-outputs:
add-comment:
max: 30
add-labels:
max: 30
allowed: ["Stale"]
remove-labels:
max: 30
allowed: ["Stale"]
close-issue:
max: 30
Each cap is enforced independently. If you hit the add-comment limit on Bucket B issues (newly-stale), the agent can still close Bucket A issues (already-stale) up to its own cap. After changing any max: value, run gh aw compile to regenerate the lock file.
What Requires Recompilation#
Changes to the Markdown body (policy thresholds, exempt labels, comment tone, instructions) take effect on the next run with no recompilation. Changes to the YAML frontmatter (engine, safe-output caps, permissions, schedule) require gh aw compile before they take effect .
Key Behavioral Differences#
Understanding these differences will help you set expectations before running better-stale-bot on your repository for the first time.
Processing Order#
actions/stale processes issues in time order (most recently created or updated first, depending on configuration). better-stale-bot ranks all potentially stale issues by an engagement score and processes the quietest threads first :
engagement_score = (3 × distinct_users) + (2 × total_comments_and_reactions) + (1 × whole_weeks_since_last_updated)
Low engagement score = fewer distinct users and fewer reactions = quietest thread = labeled first. High-engagement threads are processed later, giving active discussions more time to resolve naturally. The agent shows the full score calculation for each issue in its run log before acting.
Comment Content#
actions/stale posts the same configured template on every issue. better-stale-bot generates a unique comment per issue containing :
- An opening statement marking the issue stale
- A 2–4 sentence summary of what the issue is about and what was discussed
- Whether the issue looks resolved or is still unresolved
- Next steps — how long until automatic closure and how to keep the issue open
- A closing line of appreciation
Resolved vs. Unresolved Classification#
actions/stale has no concept of whether an issue was answered. better-stale-bot explicitly classifies each thread as RESOLVED or UNRESOLVED before generating the stale comment, and includes that classification in the comment body . An issue classified as RESOLVED will receive a stale comment that reflects this ("this looks resolved but hasn't been closed"), not a generic "no activity" message.
Language#
actions/stale comments are in whatever language you write the template. better-stale-bot detects the primary language from the issue title and generates the comment in that language . If the thread contains multiple languages, the title language wins. No template localization is needed.
Re-engagement Behavior#
actions/stale removes the stale label based on configurable activity rules. better-stale-bot removes the Stale label when any qualifying non-bot user activity (e.g., a new comment) occurs strictly after the Stale label was applied — no comment is posted, the label is silently removed . Activity that predates the latest label application does not count.
Write Limits#
actions/stale uses a single operations-per-run cap across all write types. better-stale-bot has four separate caps — comments, labels added, labels removed, and issues closed — each enforced independently . The agent cannot exceed any individual cap even if others still have headroom.
Cross-Run Memory#
better-stale-bot writes a JSON run summary to cache-memory at the end of each run . Subsequent runs read this file to avoid reprocessing issues that were recently labeled, closed, or un-staled. actions/stale has no equivalent cross-run state.
Step-by-Step Migration#
Step 1: Disable the existing actions/stale workflow#
Before installing better-stale-bot, turn off actions/stale so the two bots don't compete. The safest approach is to delete the workflow file entirely, or disable it in the GitHub Actions UI .
# Option A: delete the workflow file
rm .github/workflows/stale.yml # adjust filename as needed
git commit -am "Remove actions/stale workflow"
git push
# Option B: disable from the GitHub Actions tab
# Go to Actions → Stale → "..." menu → Disable workflow
Step 2: Install the GitHub CLI and gh-aw extension#
better-stale-bot is distributed as a GitHub Agentic Workflow and requires the gh CLI with the gh-aw extension.
# Authenticate with GitHub (if not already done)
gh auth login
# Install the gh-aw CLI extension
gh extension install github/gh-aw
Step 3: Add the better-stale-bot workflow#
Run the interactive wizard from a local clone of the repository where you want the bot. You need write access so the wizard can open a pull request .
gh aw add-wizard dosu-ai/better-stale-bot/better-stale-bot
The wizard will:
- Check prerequisites
- Prompt you to configure the required API key secret
- Download the workflow file
- Compile it
- Open a pull request
Add --skip-secret if you prefer to set the secret manually in the GitHub UI.
Alternatively, use the manual setup:
mkdir -p .github/workflows
curl -fsSL https://raw.githubusercontent.com/dosu-ai/better-stale-bot/main/workflows/better-stale-bot.md \
-o .github/workflows/better-stale-bot.md
gh aw compile better-stale-bot
git add .github/ .gitattributes
git commit -m "Add better-stale-bot workflow"
git push
Step 4: Configure your settings#
Open .github/workflows/better-stale-bot.md and edit the ## Configuration section to match your old actions/stale settings :
| What to change | Where |
|---|---|
Inactivity threshold (days-before-stale) | ## Configuration table, Default column |
Grace period (days-before-close) | ## Configuration table, Default column |
| Exempt labels | ## Configuration, exempt labels bullet |
| Comment tone or structure | The prose instructions below ## Step 3 in the Markdown body |
| Per-run write caps | YAML frontmatter safe-outputs → max: values |
Changes to the Markdown body take effect on the next run without recompilation. Only frontmatter changes (safe-outputs caps, engine, schedule) require a recompile.
Tip: If your old days-before-stale was longer than 60 days, set it to match your previous value on the first run to avoid a sudden wave of stale labels on issues that were previously within your threshold.
Step 5: Set the required API key secret#
The default engine is Claude Haiku, which requires an ANTHROPIC_API_KEY secret. Add it to the repository :
gh aw secrets set ANTHROPIC_API_KEY
Or set it via the GitHub UI: Settings → Secrets and variables → Actions → New repository secret.
If you want to use a different engine, update the engine: field in the frontmatter and add the corresponding secret:
| Engine | Required Secret |
|---|---|
| Claude (default) | ANTHROPIC_API_KEY |
| GitHub Copilot | COPILOT_GITHUB_TOKEN |
| OpenAI Codex | OPENAI_API_KEY |
Step 6: Compile (if you changed the frontmatter)#
If you edited the YAML frontmatter (safe-output caps, engine, schedule, permissions), regenerate the lock file:
gh aw compile
You do not need to recompile if you only changed the Markdown body.
Step 7: Merge the pull request#
If you used the add-wizard, merge the PR it opened. If you pushed directly, the workflow is already active.
Step 8: Test with a manual run#
Trigger a run immediately without waiting for the daily schedule:
gh aw run better-stale-bot
Or go to Actions → better-stale-bot → Run workflow in the GitHub UI.
Step 9: Monitor the first few runs#
Open the run in GitHub Actions and expand Agent Summary. You'll find:
- Agentic Conversation — the full reasoning trace, including engagement score calculations and per-issue summaries
- Token Usage — how many input/output tokens were used (useful for estimating cost)
- Safe Outputs — a log of every write operation the agent requested and whether it was applied
If the run produces unexpected results, read the agent conversation to understand what the agent saw and decided. All reasoning is transparent and inspectable.
Handling Existing Stale Labels#
If your repository has issues already labeled with a stale label from actions/stale, here is what to expect.
Same label name (Stale)#
If actions/stale used the same Stale label name (capital S, matching better-stale-bot's default), better-stale-bot will immediately pick those issues up in Bucket A — the "already stale" bucket processed in Step 4 of the workflow .
For each of those issues, better-stale-bot determines stale_label_applied_at by finding the most recent labeled event for the Stale label in the issue timeline — not from a bot comment, not from the first time the label was ever applied. If the issue was labeled, unlabeled, and labeled again, the most recent application wins .
Once stale_label_applied_at is known, the normal grace period rules apply: if at least days-before-close days have passed with no qualifying non-bot activity after that timestamp, the issue is closed. If non-bot activity occurred after the label was applied, the Stale label is silently removed.
Practical impact: On your first run, a batch of previously-stale issues may be closed immediately if they were labeled more than days-before-close days ago with no new activity. If you want to review them before closure, either temporarily increase days-before-close in the Markdown body, or remove the Stale labels from issues you want to preserve before the first run.
Different label name (e.g., lowercase stale)#
actions/stale defaults to a lowercase stale label. better-stale-bot defaults to title-case Stale. If your repository has issues labeled stale (lowercase) and better-stale-bot is configured for Stale (uppercase), the bot will not recognize them as already-stale.
To resolve this, either:
- Rename existing labels: Use the GitHub Labels UI or API to rename
stale→Stalebefore the first run. - Update the bot configuration: Change the stale label name in the
## Configurationsection ofbetter-stale-bot.mdand update theallowedlist in theadd-labelsandremove-labelssafe-outputs in the YAML frontmatter, then recompile.
# frontmatter example after changing label name to "stale" (lowercase)
safe-outputs:
add-labels:
max: 30
allowed: ["stale"]
remove-labels:
max: 30
allowed: ["stale"]
Then update the ## Configuration section in the Markdown body to match, and run gh aw compile.
Pull Request Support#
The shipped better-stale-bot workflow handles issues only. Permissions, GitHub tools, safe-outputs, and instructions are all scoped to issues by default .
If your actions/stale configuration also handled pull requests via days-before-pr-stale and days-before-pr-close, you will need to extend the workflow manually. Five changes are required in better-stale-bot.md :
1. Widen permissions in the YAML frontmatter
permissions:
contents: read
issues: read
pull-requests: read
2. Grant the pull-requests toolset
tools:
github:
toolsets: [issues, pull-requests]
cache-memory: true
3. Add PR safe-outputs
safe-outputs:
# ... existing issue outputs ...
close-pr:
max: 30
4. Update the Markdown instructions
Extend the Step 1 bucket definitions to include pull requests, and add PR-specific handling logic to Steps 3 and 4 (or add a separate Step for PRs). For example, add pull requests to Bucket B and add a bucket for already-stale PRs.
5. Recompile
gh aw compile
After recompilation, commit both .github/workflows/better-stale-bot.md and .github/workflows/better-stale-bot.lock.yml.
Note: Pull request stale handling was not part of the original better-stale-bot design and is not tested by the project. Start conservatively — use the
staged: trueflag insafe-outputsto preview what the workflow would do before enabling live PR closures.
FAQ#
Can I run both actions/stale and better-stale-bot at the same time?#
No. Running two stale bots simultaneously will create duplicate stale comments, competing label applications, and confusing issue state. Disable or delete your actions/stale workflow before enabling better-stale-bot .
What if I want the same static message on every issue?#
You can add an instruction in the ## Step 3 section of the Markdown body telling the agent to use a specific template, and the agent will follow it. However, the AI may still adapt slightly based on the thread content. If you need a truly static, never-changing message regardless of issue context, actions/stale may be a better fit for that requirement.
How much does it cost?#
Cost depends on the model, the number of issues a run touches, and the length of the issue threads being read. The default engine is Claude Haiku, which offers a good cost-to-quality tradeoff for this use case. Monitor usage via GitHub Actions → Agent Summary → Token Usage on any run, or use gh aw logs and gh aw audit from the CLI .
Does it support exempt-milestones or exempt-assignees?#
Not as explicit configuration options. better-stale-bot exempt logic is defined entirely in natural language in the Markdown body. You can add instructions like "Do not mark issues with a milestone as stale" or "Skip issues assigned to @..." and the agent will follow them. Add those instructions to the ## Configuration section or as additional guidelines in the Markdown body — no recompilation is needed.
What happens if a run is interrupted or the safe_outputs job fails?#
If the safe_outputs job fails due to a transient API error or threat detection blocking, you can replay the safe outputs from that run without re-running the full agent. Use the Agentic Maintenance workflow and provide the URL of the failed run. No issues will be double-processed as a result of the replay.
How do I preview what the bot would do before it makes real changes?#
Add staged: true to the safe-outputs block in the YAML frontmatter:
safe-outputs:
staged: true
add-comment:
max: 30
# ...
In staged mode the agent runs in full but no writes are applied to GitHub. Review the proposed outputs in the run artifacts, then remove staged: true and recompile to go live. Remember to recompile after adding or removing staged: true.