Publications
After this page you can turn a reviewed working tree into a pull request, poll it to a terminal state, and land it at exactly the commit a human approved — or find out that it moved, instead of merging something nobody read.
Two commands, not one
Section titled “Two commands, not one”Publishing and merging are separate durable objects, and the separation is the design:
| Object | What it does |
|---|---|
| Publication | Pushes the session’s work to a branch and opens (or updates) a pull request. |
| Merge | Integrates that pull request at one exact head commit. |
They are split because opening a pull request and landing it have different authority, different retry behavior, and different things that can block them — a review, a required check, a branch protection rule. Folding them into one call would mean an integration could not express “publish now, land later, only if the head is still the one that was reviewed”, which is the normal case.
Both are created with PUT to an id you mint. Same id and same body replays as
200 with the object you already have; the same id with a different body is a
409. Publications and merges are therefore safe to retry after a timeout, in
the same way turns are — see
Sessions, turns, runs.
Publishing
Section titled “Publishing”You publish a revision, not a diff and not a branch name. Read it first from the session’s repository review — the 64-character hex digest of the working tree described in Repositories — and pass it back. That is the “I reviewed exactly this” token: if the agent touches the tree after you read the review, the revision changes and your publish request no longer matches what you looked at.
The body is four fields:
| Field | Required | Notes |
|---|---|---|
revision |
yes | 64 hex characters, from the review. |
title |
yes | 1–256 characters, no line breaks. |
body |
yes | Up to 65,536 characters. May be "". |
draft |
no | Defaults to true. |
The branch is not yours to choose. It is derived from the session, stable across
republishes of that session, and lives in a reserved namespace
(wamp/publications-v1/…). Publishing the same session twice updates the same
branch and the same pull request rather than opening a second one, which is why a
successful result tells you whether it was reused.
Publishing requires a repository grant that includes PUBLISH. Without one the
request is 403 cloud_repository_grant_required, and no administrator action
happens on your behalf.
Publication lifecycle
Section titled “Publication lifecycle”admitted ──▶ prepared ──▶ attempted ──┬──▶ succeeded └──▶ failedadmitted means the command is committed and will be worked on; the three
non-terminal states are the server making progress, not something you drive. You
poll the publication until status is succeeded or failed, exactly as you
poll a run.
At most one non-terminal publication per session. A second concurrent publish
is 409 cloud_publication_in_progress. That is a queue you do not have to build.
On success, result appears:
{ "publication": { "id": "e8b1c4d6-0a2f-4c3b-9d1e-5f6a7b8c9d01", "status": "succeeded", "revision": "5c1e…", "title": "Fix CSV export rounding", "result": { "branch": "wamp/publications-v1/cloud-3f7d4f4c-2b6a-1a2b3c4d", "commitSha": "7c9e1a3b5d7f9012345678901234567890abcdef", "pullRequest": { "number": 412, "url": "https://github.com/…/pull/412", "draft": true, "reused": false } }, "completedAt": "2026-08-11T09:12:44.010Z" }}result is present only when the publication actually produced a pull request —
in practice, only on succeeded. On failed, read lastError.code. The
pullRequest.reused flag is the one field people forget: true means an existing
pull request was updated, so your product should say “updated PR #412”, not
“opened PR #412”.
Two other places the same fact arrives: the event log gets
wamp.publication.created (or wamp.publication.failed with an errorCode), and
the session gains a github.pull_request artifact carrying the publicationId.
See Artifacts.
Merging
Section titled “Merging”A merge names the head it was prepared against. You do not supply that head — the
server takes expectedHeadSha from the publication, and the merge is submitted to
GitHub against that exact commit. If someone pushed to the branch in between, the
merge fails rather than integrating commits nobody reviewed. That is the whole
point of the separation.
The body is optional, and all four fields have safe defaults or are omitted:
| Field | Default | Notes |
|---|---|---|
strategy |
squash |
merge, squash, or rebase. |
mode |
now |
now or when_ready. |
commitTitle |
— | 1–256 characters, no line breaks. |
commitMessage |
— | Up to 65,536 characters. |
GET /v1/capabilities advertises the strategies and modes the server accepts;
read them from there rather than hardcoding the list.
A merge can only be admitted for a publication that has already succeeded and
has a pull request. Asking earlier is 409 cloud_publication_merge_not_ready.
Merge lifecycle
Section titled “Merge lifecycle”admitted ──┬──▶ attempted ──┬──▶ succeeded └──▶ waiting ────┴──▶ failedstatus |
Meaning |
|---|---|
admitted |
Committed, not yet attempted. |
attempted |
The merge has been submitted. |
waiting |
Legal, but not yet allowed to land: a required check, a review, or a branch protection rule has not cleared. Only mode: when_ready parks here; it is retried. |
succeeded |
Landed. mergedCommitSha is the resulting 40-character commit id. |
failed |
Did not land. lastError.code says why. |
Three invariants worth building on, each enforced in the database rather than by convention:
mergedCommitShais present exactly when the merge succeeded. Its presence is equivalent to success; you do not need to trust the status field alone.- At most one successful merge per publication, ever. Not “one at a time” —
once. A publication that has landed cannot be landed again, and a request to do
so is a
409. - At most one live merge per publication. While one is
admitted,attemptedorwaiting, a second merge id for that publication is409 cloud_publication_merge_conflict.
attempts is exposed because the server retries transient failures on its own. A
rising attempts on a waiting merge is the system working, not your problem.
The failures that matter
Section titled “The failures that matter”They arrive in lastError.code on the merge, and in the errorCode of
wamp.publication.merge_failed:
lastError.code |
What happened |
|---|---|
pull_request_changed |
The head moved after the publication. Nothing was merged. Publish the new revision and merge that. |
pull_request_closed |
Someone closed the pull request. |
merge_not_ready |
The pull request cannot land yet — most commonly because it is still a draft, which is the default a publication opens with. |
merge_not_allowed |
The repository refuses this merge, for example a strategy the repository does not permit. |
The draft case is the one that surprises people: draft defaults to true, and a
draft pull request is not mergeable. With mode: now the merge ends failed with
merge_not_ready; with mode: when_ready it sits in waiting and lands once the
pull request is marked ready and its checks pass. Choose when_ready when a human
or a CI pipeline is expected to act, and now when you believe the pull request is
already landable and want a definite answer.
A worked sequence
Section titled “A worked sequence”Review, publish, poll, merge — four calls, two ids you mint.
PUB_ID=$(uuidgen | tr 'A-Z' 'a-z')MERGE_ID=$(uuidgen | tr 'A-Z' 'a-z')
REVISION=$(curl -sS "$WAMP_API/v1/sessions/$SESSION_ID/repository/review" \ -H "Authorization: Bearer $WAMP_TOKEN" | jq -r '.review.revision')
curl -sS -X PUT "$WAMP_API/v1/sessions/$SESSION_ID/publications/$PUB_ID" \ -H "Authorization: Bearer $WAMP_TOKEN" \ -H 'Content-Type: application/json' \ -d "$(jq -nc --arg r "$REVISION" '{ revision: $r, title: "Fix CSV export rounding", body: "Adds a regression test for the invoice total.", draft: false }')"
# Poll until status is succeeded or failed.curl -sS "$WAMP_API/v1/sessions/$SESSION_ID/publications/$PUB_ID" \ -H "Authorization: Bearer $WAMP_TOKEN" | jq '.publication | {status, result, lastError}'
curl -sS -X PUT \ "$WAMP_API/v1/sessions/$SESSION_ID/publications/$PUB_ID/merges/$MERGE_ID" \ -H "Authorization: Bearer $WAMP_TOKEN" \ -H 'Content-Type: application/json' \ -d '{"strategy":"squash","mode":"when_ready"}'Note draft: false in the publish body: it is what makes the merge above able to
land at all. Then poll the merge the same way, and watch for
wamp.publication.merge_succeeded or wamp.publication.merge_failed in the event
log if you are already following the session.
What you may assume, and what you must handle
Section titled “What you may assume, and what you must handle”Assume. A publication’s revision and a merge’s expectedHeadSha are fixed
at admission. A merge never lands a commit other than that head. A publication
that succeeded twice does not exist, and a publication that merged twice cannot.
Retrying an identical PUT is free.
Handle. 409 cloud_publication_in_progress while one is in flight.
409 cloud_publication_conflict and 409 cloud_publication_merge_conflict from a
replay whose body drifted — usually a re-read review with a new revision, which
needs a new publication id. 403 cloud_repository_grant_required if publishing
authority was revoked between session creation and publish.
409 cloud_publication_merge_not_ready when you merge too early. And a merge that
ends failed with pull_request_changed, which means starting the review-publish
cycle again rather than retrying the same merge id.
Related
Section titled “Related”- Repositories — grants,
PUBLISHauthority, and whererevisioncomes from. - Open a pull request — the same flow end to end, with the polling written out.
- Sessions, turns, runs — caller-owned ids and
idempotent
PUT. - Publications operations — the generated endpoint reference, and the API reference for the publication and merge objects field by field.