In this walkthrough, you’ll submit three changes as a stack of GitHub pull requests, revise the middle change, and merge the first PR while the others stay open. The examples call these changes A, B, and C. Follow along with three changes of your own.

Install

You need Python 3.14 or newer, jj 0.45.1 or newer, and a GitHub repo you can push to. Install jj-stack with uv:

uv tool install jj-stack
Other installation options

You can also install with pipx install jj-stack, or use python -m pip install jj-stack inside an activated virtual environment.

To upgrade a uv installation, run uv tool upgrade jj-stack. If your shell cannot find jj-stack, run uv tool update-shell.

If you use the GitHub CLI and have not signed in, run:

gh auth login

jj-stack can use that login, or a token in GITHUB_TOKEN or GH_TOKEN. GitHub stacked PRs are in public preview and require no repo or organization setup.

Inside your jj repo, check its remote, trunk, and GitHub access:

jj-stack doctor --fix
Scroll through the full transcript
# Check the repo and apply the safe local setup
 jj-stack doctor --fix
check                   status detail
─────────────────────────────────────────────────────────────────────
remote                  ok     origin
PR branch fetch         fixed  jj git fetch skips jj-stack/* branches
PR bookmarks            ok     none
checkout/sync leftovers ok     none
GitHub remote           ok     octo-org/stacked-prs
GitHub auth             ok     token found (GITHUB_TOKEN)
connectivity            ok     reached octo-org/stacked-prs
push access             ok     can push to octo-org/stacked-prs
GitHub stacks           ok     stacked pull requests available
trunk branch            ok     main

The --fix option configures your repo so that jj-stack’s PR branches stay out of ordinary fetches and local bookmark output. Resolve any remaining failed checks before continuing.

Build A → B → C

Start a fresh line of work above trunk:

jj new 'trunk()'

Make your first change, then run jj commit to give it a description and start a new working copy above it. Repeat this for the other two changes:

# edit files
jj commit -m "A: refactor shared model"
# edit files
jj commit -m "B: add API"
# edit files
jj commit -m "C: add UI"
Scroll through the full transcript
# Start a fresh line of work above trunk
 jj new 'trunk()'
Working copy  (@) now at: xumkpxxn 81c15bbb (empty) (no description set)
Parent commit (@-)      : qymknmmp ad21b23a main | base
# Use the working copy as scratch, then commit each finished change
 printf 'shared model\n' > model.py
 jj commit -m 'A: refactor shared model'
Working copy  (@) now at: uruvtuol 47a1a3a3 (empty) (no description set)
Parent commit (@-)      : xumkpxxn 95200d3a A: refactor shared model
 printf 'API\n' > api.py
 jj commit -m 'B: add API'
Working copy  (@) now at: rtqztslu f0e0540f (empty) (no description set)
Parent commit (@-)      : uruvtuol a3aa097e B: add API
 printf 'UI\n' > ui.py
 jj commit -m 'C: add UI'
Working copy  (@) now at: tquwuwos e012cfbb (empty) (no description set)
Parent commit (@-)      : rtqztslu ebaa39e0 C: add UI
 jj log
@  tquwuwos
  rtqztslu  C: add UI
  uruvtuol  B: add API
  xumkpxxn  A: refactor shared model
  qymknmmp  base

~

Your history now has three changes above trunk and an empty working copy above C:

flowchart BT
  T["trunk()"] --> A["A: model"] --> B["B: API"] --> C["C: UI<br/>stack head"]
  C --> W["@<br/>empty working copy"]

Run jj log to find the change IDs for B and C. You’ll use them later to edit B and submit the stack with C at its head.

Inspect and submit

Run jj-stack with no subcommand to see your stack:

jj-stack

You’ll see A, B, and C, with no pull requests yet. Because your working copy is empty, jj-stack uses its parent, C, as the top of the stack.

Submit your stack for review:

jj-stack submit
Scroll through the full transcript
# Inspect the selected stack
 jj-stack
Unsubmitted stack:
  ktqnnnsy  C: add UI

  slwtxkzq  B: add API

  xywrmktp  A: refactor shared model

  tnzttmly  base
# Create one pull request per change
 jj-stack submit
Selected: C: add UI (ktqnnnsy)
Submitted changes:
  ktqnnnsy  C: add UI: PR #3

  slwtxkzq  B: add API: PR #2

  xywrmktp  A: refactor shared model: PR #1

  tnzttmly  base

Top of stack: PR #3
Created GitHub stack #1.

The output now shows one PR per change. We’ll call them PRs #1, #2, and #3, though GitHub will assign different numbers in your repo:

ChangePull requestBaseWhat reviewers see
A: refactor shared model#1mainThe model refactor
B: add API#2PR #1’s branchThe API changes relative to A
C: add UI#3PR #2’s branchThe UI changes relative to B

submit creates the PR branches and groups these PRs into a GitHub stack. Each change’s subject becomes its PR title, and the rest of its description supplies the body.

If your terminal has hyperlink support (such as Ghostty, iTerm2, or kitty), you can click the PR number beside Top of stack to open it on GitHub.

Revise B while C depends on it

Suppose a reviewer asks for an API correction. Replace the placeholders below with the change IDs from jj log:

jj edit <B-change-id>
# edit files
jj-stack submit <C-change-id>
Scroll through the full transcript
# Edit B while C depends on it
 jj edit wyturxyz
Working copy  (@) now at: wyturxyz fb784c8d B: add API
Parent commit (@-)      : tswswlmq 518c2932 A: refactor shared model
Added 0 files, modified 0 files, removed 1 files
 printf 'typed response\n' >> api.py
# Select C to update the whole stack, even while the working copy is at B
 jj-stack submit qtopwpxl
Submitted changes:
  qtopwpxl  C: add UI: pushed, PR #3 unchanged

@  wyturxyz  B: add API: pushed, PR #2 unchanged

  tswswlmq  A: refactor shared model: already pushed, PR #1 unchanged

  oktnlvsz  base

Top of stack: PR #3
# PR #1 is unchanged; B and C still have PRs #2 and #3
 jj-stack view qtopwpxl
Submitted stack (PR #3):
  qtopwpxl  C: add UI: PR #3

@  wyturxyz  B: add API: PR #2

  tswswlmq  A: refactor shared model: PR #1

  oktnlvsz  base

jj edit takes the change you want to edit. jj-stack submit takes the top of the stack you want to publish. Here, you edit B but pass C to submit the whole A → B → C stack.

Inspect the result:

jj-stack view <C-change-id>

The output should still list PRs #1, #2, and #3, and their discussions remain on GitHub. PR #1 is unchanged, while PRs #2 and #3 now point to the new commits for B and C. Even though you edited only B, C also needed a new commit because its parent changed.

In submit’s output, pushed means the PR branch received the new commit. PR unchanged means its title, body, and other PR details needed no update.

Merge A and keep working on B and C

Once A meets the repo’s review and check requirements, you can merge its PR. Replace 1 with its PR number in your repo. This example chooses squash merging; without --method, jj-stack picks a merge method your repo allows:

jj-stack merge --pull-request 1 --method squash
Scroll through the full transcript
# A, B, and C are submitted as PRs #1, #2, and #3
# A is ready to merge; keep B and C open
 jj-stack merge --pull-request 1 --method squash
Using PR #1 for change solxnvlm
Waiting for GitHub to merge. Ctrl-C stops waiting without cancelling.
Trunk: main, observed at "base"
Merge completed:
  ✓ GitHub merge request: merged PR #1 into main through change solxnvlm (commit ID 3ee70f78)
GitHub reported final trunk commit 7d11720f.
Updating the local stack after the completed merge:
Removing merged changes from the bottom of the stack: solxnvlm
Submitted changes:
  yoquuonk  C: add UI: already pushed, PR #3 unchanged

  mrtszvus  B: add API: already pushed, PR #2 unchanged

  lqtyzmym  A: refactor shared model (#1)

Top of stack: PR #3
Cleanup:
  ✓ remote branch: delete jj-stack/a-refactor-shared-model-solxnvlm@origin
  ✓ forget the saved link between PR #1 and solxnvlm
# The direct merge updated the local stack and the same remaining PRs
 jj-stack view yoquuonk
Submitted stack (PR #3):
  yoquuonk  C: add UI: PR #3

  mrtszvus  B: add API: PR #2

  lqtyzmym  A: refactor shared model (#1)

The --pull-request option names the last PR you want to merge, starting from the bottom of the stack. Here, choosing PR #1 merges only A. Choosing PR #2 would ask GitHub to merge A and B together.

The command waits for GitHub to merge, including through a merge queue, then fetches the result and rebases B and C onto the updated trunk. It also updates their existing PRs to match:

flowchart BT
  M["main<br/>includes A"] --> B["B: API<br/>same PR #2"] --> C["C: UI<br/>same PR #3"]

PR #1 is merged. PR #2 now targets main, and PR #3 still targets PR #2’s branch. Run jj-stack view <C-change-id> to check that only B and C remain in the local stack.

If your repo uses a merge queue, the queue chooses the merge method and --method is ignored. To return before GitHub finishes, use --no-wait or press Ctrl-C; neither cancels the request. If you returned early, or merged through GitHub or its Rebase stack action, run this once GitHub finishes:

jj-stack sync <C-change-id>

To continue from the top, create a fresh scratch change above C:

jj new <C-change-id>

You can now add another change, or keep revising B and C. Run jj-stack submit whenever you’re ready to publish those changes for review.

What next?