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:
| Change | Pull request | Base | What reviewers see |
|---|---|---|---|
| A: refactor shared model | #1 | main | The model refactor |
| B: add API | #2 | PR #1’s branch | The API changes relative to A |
| C: add UI | #3 | PR #2’s branch | The 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?
- Read how jj-stack works for selection, change IDs, and PR branches.
- Follow submit and update for drafts and reviewer requests.
- Read merge and sync for merge methods, queues, and recovery.
- Set up the optional
jj stackalias and shell completion.