Skip to content

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.

Publishing and merging are separate durable objects, and the separation is the design:

Object What it does
Publication Delivers the session’s exact reviewed revision through a pull request (default) or an explicitly authorized direct branch update.
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.

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 has four content fields plus an optional delivery choice:

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 and 65 536 UTF-8 bytes. May be "".
delivery no pull_request (default) or direct_branch. Direct delivery is covered by Push directly to a branch.
draft no Defaults to true. It describes a pull request, and direct branch delivery opens none — so the field is refused there rather than accepted and discarded: with delivery: "direct_branch", sending draft at all, with either value, is 400 invalid_request.

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.

Because the pull request is reused, draft is reconciled on every publish rather than only honored on the first. Republishing the same session with draft: false takes the existing pull request out of draft, which is what makes it mergeable. The reverse does not happen: nothing this API does puts a ready pull request back into draft.

Both deliveries run on one authority: the Session’s repository grant, live at the moment of the publish. Direct delivery is accepted only for a Session bound to such a grant — one whose repository comes from a person’s own GitHub connection instead is refused it outright. The direct request has no branch/ref parameter. The server uses the base branch and OID pinned when the Session was created, verifies that the reviewed head has that exact single parent, and uses an exact expected-head lease for the update. GitHub branch protections and rulesets remain authoritative.

PUT /v1/sessions/{sessionId}/publications/{publicationId} reads the reviewed tree out of the sandbox, so it needs one. A session whose lease has lapsed answers 409 cloud_workspace_expired, and one that has no lease at all answers 503 cloud_workspace_unavailable — the same pair GET /v1/sessions/{sessionId}/repository/review returns.

Neither is a lost publication. PUT /v1/sessions/{sessionId}/workspace provisions a fresh sandbox and replays the session’s checkpoint into it, after which you re-read the review and publish the revision it now reports. Read Sandboxes and environments for when that recovery is available; it is refused with 409 cloud_resume_unavailable when continuation.canContinue is false.

Publish while the session is still warm if you can. It is the cheaper path, and recovery re-reads the review because the revision is a digest of the tree the sandbox actually holds.

admitted ──▶ prepared ──▶ attempted ──┬──▶ succeeded
└──▶ failed

admitted 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",
"delivery": "pull_request",
"revision": "5c1e…",
"title": "Fix CSV export rounding",
"result": {
"delivery": "pull_request",
"branch": "wamp/publications-v1/cloud-3f7d4f4c-2b6a-4a2e-9c1a-1f2b3c-6b031ca6",
"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 on succeeded. A direct result instead has delivery: "direct_branch", branch, and commitSha, with no fabricated pullRequest. 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.

Direct delivery emits the same wamp.publication.created event with kind: "github.branch" and delivery: "direct_branch"; it does not create a pull-request artifact. If the base moved or GitHub policy rejects the update, review again and create a new Publication. WAMP never retries it without the exact expected base OID.

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 and 65 536 UTF-8 bytes.

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.

admitted ──┬──▶ attempted ──┬──▶ succeeded
└──▶ waiting ────┴──▶ failed
status 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:

  • mergedCommitSha is 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, attempted or waiting, a second merge id for that publication is 409 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.

Those last two invariants leave one trap, and it has a way out. A merge id is yours to mint, but a publication admits only one live merge — so if you lose the id you minted, you can neither address the command nor mint a replacement. GET …/publications/{publicationId}/merges is the recovery path: it pages every merge for that publication, including failed attempts, so you can find the live one and resume polling it.

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.

Merge itself never clears a draft — it does one thing, against one head. The route out of merge_not_ready on a draft is therefore two steps, in this order: publish again with draft: false, wait for that publication to reach succeeded, and only then admit a new merge id. A backend that must land its own work should just publish with draft: false from the start.

Review, publish, poll, merge — four calls, two ids you mint.

Terminal window
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. 409 cloud_workspace_expired or 503 cloud_workspace_unavailable when the sandbox holding the reviewed tree is gone — recover with PUT /v1/sessions/{sessionId}/workspace, then review and publish again. 403 cloud_repository_grant_required if the grant was revoked between session creation and publish. For direct delivery, also handle a protected or moved base as a review-again conflict; never change the request to a caller-selected ref or force mode. 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.