The short answer
Neon publishes four GitHub Actions: create a branch, delete a branch, reset a branch to its parent, and diff two branches’ schemas with the result posted as a pull request comment.1 Run “create branch” when a pull request opens, apply the pull request’s migrations and tests against that branch, post the schema diff, and delete the branch when the pull request closes. A Neon branch is a copy-on-write clone created from a current or past state without load on the parent.3
Two defaults in the create action matter for agent work: the role input defaults to neondb_owner, and expires_at defaults to empty, meaning the branch never expires.2 Set both deliberately.
Deedglass
A database change, checked and put back
-
1The agent’s change
Change order 42
UPDATE orders … WHERE id = 42
The agent: “1 row updated” -
2What the database really did
301 rows
A hidden trigger changed 300 more
Doesn’t match -
3Put back
301 rows restored
Signed off by a person
Matches
The four actions
| Action | What it does | When to run it |
|---|---|---|
| Create branch | Creates a branch, e.g. for previews or tests | Pull request opened or updated |
| Schema diff | Compares two branches’ schemas and comments the diff on the pull request | After migrations run on the new branch |
| Reset branch | Resets a branch to its parent’s latest state | When you want fresh data on a long-lived branch |
| Delete branch | Deletes a branch | Pull request merged or closed |
All four from Neon’s docs.1 Neon suggests storing the API key as a repository secret NEON_API_KEY and the project ID as a variable NEON_PROJECT_ID.1
The inputs that matter for agents
From the create-branch action’s README:2
| Input | Default | Why it matters |
|---|---|---|
project_id, api_key |
Required | The key can create and delete branches; scope it to this project |
parent_branch |
The project’s primary branch | Your production data, copied |
role |
neondb_owner |
The connection string the action outputs uses this role |
expires_at |
Empty: no expiry | Forgotten branches of production data pile up |
masking_rules |
Undefined | An option for masking data on the branch |
branch_name |
Generated | Name it after the pull request so delete can find it |
Outputs include db_url, db_url_pooled, branch_id, password and created, which is false when an existing branch is reused.2
The role default is the one to think about. An owner role on a copy of production can do anything to that copy, which is fine for running migrations. But the same db_url handed to an agent, or to test code that logs it, is an owner credential on a full copy of your data. Our post on what neon_superuser and friends carry explains why that’s more than it sounds.
The README’s own example sets expires_at to 14 days ahead in RFC 3339 format.2 Do the same: a branch per pull request should outlive the pull request by days, not forever.
A workflow sketch
Built from the documented inputs and outputs (check action versions against the Marketplace before use; we didn’t run this against a Neon project):
Code A settings file: the rules a tool reads before it acts.
name: preview-db
on:
pull_request:
types: [opened, synchronize, reopened, closed]
permissions:
contents: read
pull-requests: write
jobs:
branch:
if: github.event.action != 'closed'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: echo "EXPIRES_AT=$(date -u --date '+14 days' +'%Y-%m-%dT%H:%M:%SZ')" >> "$GITHUB_ENV"
- id: create
uses: neondatabase/create-branch-action@v6
with:
project_id: ${{ vars.NEON_PROJECT_ID }}
api_key: ${{ secrets.NEON_API_KEY }}
branch_name: preview/pr-${{ github.event.number }}
expires_at: ${{ env.EXPIRES_AT }}
- run: npm ci && npx prisma migrate deploy && npm test
env:
DATABASE_URL: ${{ steps.create.outputs.db_url }}
Add the schema diff action after the migration step and the delete action in a job that runs on closed, using their Marketplace pages for inputs.
Don’t run it on pull_request_target
Use the pull_request trigger. pull_request_target runs workflows with the target repository’s elevated token, and that is exactly how the Nx packages were compromised in 2025: a workflow on pull_request_target interpolated a pull request title into a shell step, and the elevated token was used to reach the publishing secret.4 A workflow that holds a Neon API key able to copy production data is the same kind of secret.
With pull_request, GitHub doesn’t pass secrets to workflows triggered from forks, and the token is read-only; GitHub itself advises avoiding pull_request_target when you build or run code from the pull request.5 That means preview branches only work for branches in your own repository, which is the right trade.
Masking data on preview branches
A preview branch is a full copy of the parent’s data, which usually means production customers. The create action accepts masking_rules2; check Neon’s docs for the rule format and what it covers before relying on it. Where masking isn’t set up, at least keep preview db_urls out of anything shared: test logs, pull request comments, preview deployment environment pages that the whole team can open.
Ask whether the agent’s tests need production-shaped data at all. Often a seed script with realistic fake data catches the same migration problems with none of the exposure.
Where the agent fits
A sensible split of work when an agent writes the migration:
- The agent writes the migration and opens a pull request. It doesn’t run it anywhere that matters.
- CI creates a branch from production and applies it. Migrations meet real-shaped data, with real row counts and real constraint violations, on a copy.
- The schema diff comments on the pull request. A person reads what changed.1
- Tests run against the branch.
- A person merges; your normal deploy applies the migration to production.
- The branch is deleted on close, or expires.
If the agent needs database access while it works, give it its own Neon branch, not the CI one, through an MCP server scoped to that project; our Neon MCP server post covers the settings. If something goes wrong on a branch, reset it or delete it; restoring Neon covers the production case.
A checklist
- Workflow triggered by
pull_request, notpull_request_target. -
expires_atset on every created branch. - A narrower
rolewhere the job doesn’t need owner rights. -
db_urlnever echoed to logs or comments. - Schema diff posted for every migration pull request.
- Delete action on close; expiry catches the rest.
- The Neon API key scoped to this project and stored as a secret.
To check a repository’s workflows and agent configs on each pull request, add the Deedglass scan Action; to grade an agent’s setup locally, paste its config into the check page or run npx deedglass scan (how the grade works); to try tool-call rules in your browser, open the playground.
Written with AI assistance and checked against our claims register.