A Neon database branch per pull request: where an agent's changes meet data first

When an agent opens a pull request with a migration, the safest first run is on a copy of the data made for that pull request. Neon's GitHub Actions create, diff, reset and delete such branches.

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 reads what Postgres actually committed, not what the agent reported. In our demo a hidden trigger turned 1 row into 301; a person countersigned the undo and all 301 rows came back.

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:

  1. The agent writes the migration and opens a pull request. It doesn’t run it anywhere that matters.
  2. 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.
  3. The schema diff comments on the pull request. A person reads what changed.1
  4. Tests run against the branch.
  5. A person merges; your normal deploy applies the migration to production.
  6. 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, not pull_request_target.
  • expires_at set on every created branch.
  • A narrower role where the job doesn’t need owner rights.
  • db_url never 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.

The takeaway

Neon's create-branch action defaults to the neondb_owner role and no expiry. Set expires_at and a narrow role; run on pull_request, not pull_request_target.

Read nextAn AI agent deleted data on Neon: restore the branch or copy rows back Try it: install in 7 s

Related on this site

More posts

Sources

  1. 1Neon docs: Automate branching with GitHub Actions · neon.com · checked 26 October 2026
  2. 2neondatabase/create-branch-action README · github.com · checked 26 October 2026
  3. 3Neon docs: Branching · neon.com · checked 13 October 2026
  4. 4Nx advisory GHSA-cxm3-wv7p-598c (pull_request_target root cause) · github.com · checked 26 October 2026
  5. 5GitHub docs: Events that trigger workflows · docs.github.com · checked 26 October 2026

Published 26 October 2026, sources last checked 26 October 2026. If something here is out of date, write to hello@deedglass.com.

Try it on your own setup

npx deedglass scan

Grades only your own setup. Nothing is uploaded.

Pricing