# Clean up stale PlanetScale branches before they cost you money

> Every morning, find the development branches nobody is using anymore, skip anything with a migration still in flight, delete the rest, and post a Slack recap.

- Workflow type: code
- Services: PlanetScale, Slack Bot
- Categories: Engineering, Operations
- Published: 2026-08-09

## What it does

- Checks every one of your PlanetScale databases each morning and finds development branches older than a cutoff you choose, 14 days by default.
- Leaves your production branch alone, plus any branch you add to a protected list, so long lived staging branches are never touched.
- Skips any branch that still has an open deploy request, so a migration that is mid flight is never destroyed.
- Deletes what is left and posts a single Slack message showing what was removed from each database and how many branches remain.

## What you'll need

- A PlanetScale account, with permission to view and delete branches on the databases you want cleaned up
- The name of the PlanetScale organization that holds those databases
- A Slack workspace and the channel where the daily summary should land

## Prompt

Every day at 7am, clean up development branch sprawl across our PlanetScale databases before it quietly costs us money. Build this as a deterministic code workflow on a cron trigger. Every step is a known operation, so no agent reasoning is needed.

Expose these as configurable inputs with sensible defaults: the PlanetScale organization name, the age threshold in days (default 14), a list of protected branch names that must never be deleted (default should include staging), and the Slack channel that receives the summary.

Step one, use PlanetScale List databases to get every database in the configured organization. For each database, use PlanetScale List branches to get its branches. Both of these endpoints are paginated, so page through until there are no more results rather than reading only the first page.

Step two, select the deletion candidates. From each database's branches, drop the production branch, identified by the branch's production flag rather than by guessing at its name. Drop any branch whose name appears in the configured protected list. From what remains, keep only branches whose created_at timestamp is older than the age threshold, comparing against the current time in UTC.

Step three, protect in flight migrations. For each database, use PlanetScale List deploy requests and collect the branch names attached to any deploy request that is still open. Remove those branches from the candidate list. This rule matters more than the cleanup itself, so an unfinished migration is never destroyed, and if the deploy request lookup fails for a database, skip that database's deletions entirely rather than deleting without the safety check.

Step four, use PlanetScale Delete a branch on each remaining candidate. If an individual delete fails, do not abort the whole run. Record the failure and carry on to the next branch, then include the failures in the summary.

Step five, post exactly one Slack message to the configured channel using the Slack Bot Send a Message operation. Group the results by database, listing the branch names that were deleted along with their age in days, and the count of branches remaining on that database afterwards. Add a short trailing line covering what was intentionally skipped and why, separating branches held back for an open deploy request from ones excluded as protected or production. Post the message even when nothing was deleted, stating that the check ran and found nothing old enough, so that a silent failure never looks the same as a clean result. Format the message with Slack mrkdwn, using single asterisks for bold.

## How to customize

- Change the age cutoff. Fourteen days is the default, but teams that move faster often drop it to seven.
- Add branch names to the protected list so long lived staging or demo branches are always skipped.
- Move the run time or the destination channel, or switch it to weekly if daily feels noisy.

## Example output

PlanetScale branch cleanup, 7:00am

checkout-api: deleted 4 stale branches
  • fix-tax-rounding (31 days old)
  • bruno-local-test (27 days old)
  • revert-webhook-retry (22 days old)
  • schema-spike-2 (19 days old)
  6 branches remain

identity-service: deleted 1 stale branch
  • drop-legacy-sessions (16 days old)
  3 branches remain

billing-core: nothing to delete, 2 branches remain

Skipped: add-invoice-index (open deploy request), staging (protected), main (production)

## FAQ

### Could this delete our production branch by accident?

No. The production branch of every database is excluded before anything is considered for deletion, and you can add any other branch names to a protected list that is skipped as well.

### What happens if someone is in the middle of a migration?

Before deleting anything, the workflow checks the open deploy requests on each database. Any branch attached to one is left in place, so work that is still in flight is never thrown away.

### Why does leaving old branches around cost money?

PlanetScale bills development branches for the time they are in use, so branches that were created for a change that shipped months ago quietly keep adding to the bill. Removing them is the point of running this daily.

### Does PlanetScale not already clean up branches on its own?

It can delete a branch automatically once a deploy request finishes, which covers changes that made it all the way through. It does not cover branches that were abandoned and never got a deploy request, and those are usually the ones that pile up. This workflow fills that gap.

### What does the Slack message look like if nothing needed deleting?

It still posts, showing that the check ran and that nothing was old enough to remove. That way a silent failure never looks the same as a clean result.

Use this prompt in General Input: https://www.generalinput.com/prompts/clean-up-stale-planetscale-branches-before-they-cost-you-money