Open a pull request from a pushed request
Opens a pull request on the request's repository's forge (GitHub or GHES) from the request's branch (agent/<session-id> or agent/<session-id>-<branch>) into that catalog entry's branch. Only a pushed request opens one (without ?schedule=true; a request that is not pushed yet is refused with PUSH_NOT_PUSHED, as before; with ?schedule=true the call is held until the push finishes for a push request made with the agent action flows on, and refused with PUSH_NOT_PUSHED for one made with them off (the legacy flow), and PUSH_REQUEST_RESOLVED answers a request a later one superseded), and only while the catalog entry is still the one the request pinned. The platform calls GitHub with its own credential: an installation token of the catalog's GitHub App, scoped to this repository with contents: read and pull_requests: write.
The content is the structured fields summary, changes, rationale, testing, notes and related, or the legacy body text, never both. The platform lays the fields out on the repository's pull request template when its default branch has one (.github/, the repository root or docs/, a pull_request_template.md or the one file of a PULL_REQUEST_TEMPLATE/ directory; read at the default branch's commit as it is when the request is handled, never from the pull request's own branch), under the template's headings or at its <!-- werft:slot summary --> lines, keeping the template's own text, comments and checkboxes; otherwise on its default template, a heading per field that has content, in English. A template that is over 16 KiB, not a plain UTF-8 file, ambiguous (several in a directory), unreadable, or written with HTML, tables, reference definitions or comments that share a row with text is passed over for the default template, and the pull request still opens; body_source.kind is repository with the template's path, or platform with skipped saying why, and the receipt's template records the commit, path and blob read. The template's own mentions, closing keywords and references are broken like the fields', a line that looks like a secret is replaced like theirs, and it cannot pass for the platform's tail. On either layout, related items become Closes #N (an issue of the pull request's own repository, when closes is true) or Refs owner/repo#N, written by the platform from the catalog, and an item whose repository_id the session's catalog does not have is left out (body_source.related_dropped counts them). A legacy body is used as written. body_source says which one the platform wrote, and is null when the pull request was already open and its body was left alone.
title (default Agent session <session-id>) and the text are cleaned before GitHub sees them: @ mentions, cross-repository references (owner/repo#N), closing keywords (Closes #N and the like) and HTML comments are broken with a zero-width space, a line that looks like a secret is replaced by [REDACTED], an engine's own signature line (Claude Code's Generated with link) is removed, and the body is cut to GitHub's limit, the last fields first. The body ends with a line naming the session and the commit. This call never approves, merges or asks for reviews. A session's agent merges the pull request of its own push request with the built-in github MCP server's merge_pull_request, only while the workspace allows it.
Safe to retry: a request opens at most one pull request. When one is already recorded, or GitHub already has an open one for the branch on the pushed commit, it is answered with created: false (200). An open one on another commit is not adopted: PUSH_REQUEST_RESOLVED with refusal: head_moved (expected_sha, actual_sha), or BACKEND_UNAVAILABLE when GitHub only lags behind the push. A pull request this call created on a moved head ends the stage for good: later calls answer the same head_moved without asking GitHub. Every attempt, refused ones included, leaves an open_pull_request receipt; receipt_id names this one, and a refusal's error.details.receipt_id names it.
A verified credential that holds sessions:approve opens one, as for a push decision. One that may only submit to the session (an agent's key without sessions:approve, or the key that started the session) opens one too, since the request was already pushed. The token is issued through the agent's GitHub settings: with GitHub off for the agent, or the repository outside its release, the call is refused with GITHUB_REFUSED. Worker credentials are not accepted on /v1 (401).
Scope: sessions:approve (API key or cookie session). Idempotency-Key: ignored.
AuthorizationBearer <token>For servers and scripts: an API key sent as an HTTP bearer token, Bearer <key>. It holds exactly the scopes it was issued with; a key issued before scopes existed holds none. It never needs the CSRF header, and it cannot manage agents, members, teams or API keys. Issue one in the web console under Settings → API keys; each operation names the scope it needs.
Scope: sessions:approve
id*stringThe session's id.
^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$uuidpush_request_id*stringThe push request's id.
^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$uuidschedule?stringtrue holds the call until a push that is not finished yet is pushed, instead of refusing it.
X-Requested-With?"werft-web"Required with a cookie session: the literal werft-web.
"werft-web"application/json- body
title?string1 <= length <= 1000summary?stringlength <= 8000changes?array<>items <= 50rationale?stringlength <= 8000testing?stringlength <= 8000notes?stringlength <= 8000related?array<>items <= 20body?stringlength <= 60000The pull request: 201 when this call opened it, 200 with created: false when it was already open.
application/json- response
pull_request*created*booleanbody_source*|nullreceipt_id*string^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$uuidcurl -X POST 'https://example.com/v1/sessions/497f6eca-6276-4993-bfeb-53cbbbba6f08/push-requests/497f6eca-6276-4993-bfeb-53cbbbba6f08/pull-request' \ -H 'Content-Type: application/json' \ -d '{ "title": "Add retry to the sync job"}'{ "pull_request": { "push_request_id": "5b9e2c17-4f3a-4d6e-8a1b-7c0d9e2f3a41", "session_id": "0f8b6a52-3d1e-4c7a-9b2f-5e6d7c8a9b01", "repository_id": "sample-app", "number": 42, "url": "https://github.com/acme/sample-app/pull/42", "head": "agent/0f8b6a52-3d1e-4c7a-9b2f-5e6d7c8a9b01", "base": "main", "created_at": "2026-10-11T08:40:00Z", "receipt_id": "a7b8c9d0-e1f2-4a3b-8c4d-5e6f7a8b9c0d" }, "created": true, "body_source": { "kind": "platform" }, "receipt_id": "a7b8c9d0-e1f2-4a3b-8c4d-5e6f7a8b9c0d"}