> For the index of every page in Orbital's docs, read https://docs.beta.runorbital.dev/llms.txt.

# Push and open pull requests without an agent

> Take a checkout, commit, push and open a pull request with action steps, and spend no agent turn on publishing

Publishing a change is the same work every time: commit it, push the branch and open a pull request. An action step does each of these itself, so no agent turn goes on it and no prompt has to explain how.

```toml title="action.toml"
[steps.publish.commit]
kind = "action"
do = "git.commit"
message = { write = "A conventional commit message for the change", style = "quick" }
next = "steps.publish.push"
```

## Why an action and not an agent

An agent asked to publish spends a turn running commands that never change, and may run them differently each time. An action does the same thing every time, and is safe to repeat. Committing with nothing to commit does nothing. Pushing an unchanged branch does nothing. Opening a pull request reuses the one already open for the branch. So a run that comes back to its publishing steps, after a review asks for changes or after a restart, never makes a second pull request.

Actions spend no agent turns, so they do not count towards the workflow's `max_visits`.

## The actions

An action step has `kind = "action"`, and `do` names the action.

| Action | What it does | Keys |
| --- | --- | --- |
| `worktree.create` | Makes an isolated checkout of each repository folder, all on one branch for the run, and moves the run into them, so the run never touches your working copy. | None |
| `worktree.remove` | Removes the checkouts when nothing would be lost, and records `git.worktree.clean` and `git.worktree.status`. | None |
| `git.commit` | Commits every change in each repository's checkout. | `message`, which it needs |
| `git.push` | Pushes each repository's branch. | None |
| `github.pr.open` | Opens a pull request for each repository, or reuses the one open for the branch, and records `github.pr.url`, `github.pr.number` and `github.pr.state`. | `title`, which it needs, and `body`, `draft`, `base`, `auto_merge` |

`draft = true` opens the pull request as a draft; the default is `false`. `base` is the branch to merge into: unless you set it, the branch the run's checkout was made from, which is usually the repository's default branch. `auto_merge` asks GitHub to merge the pull request by `squash`, `merge` or `rebase`. After `github.pr.open`, context holds the same `github.pr` paths a [probe](/reference/step-kinds/#probe) writes, so a later step can route on `github.pr.state`.

This workflow takes a checkout, lets an agent make the change, then publishes it with three actions:

```toml title="action.toml"
version = 3
entry = "steps.checkout"

[styles.quick]
effort = "low"

[steps.checkout]
kind = "action"
do = "worktree.create"
next = "steps.implement"

[steps.implement]
prompt = "Make the change the goal describes. Do not commit."
outputs.change_summary = "text"
next = "steps.publish.commit"

[steps.publish.commit]
kind = "action"
do = "git.commit"
message = { write = "A conventional commit message for the change", style = "quick" }
next = "steps.publish.push"

[steps.publish.push]
kind = "action"
do = "git.push"
next = "steps.publish.open"

[steps.publish.open]
kind = "action"
do = "github.pr.open"
draft = true
title = { write = "A short title naming the change", style = "quick" }
body = { write = "Why the change exists, then what it changes", style = "quick" }
next = "steps.done"

[steps.done]
kind = "terminal"
```

The prompt tells the agent not to commit, so the commit, the push and the pull request all happen in the actions after it. [Action](/reference/step-kinds/#action) gives every key.

## Let a model write the text

`message`, `title` and `body` take plain text, or a table that asks a model to write it: `{ write = "A short title naming the change" }`. A write runs a short model session with no tools. It reads the run's context and returns only the text, so the commit message and the pull request describe the change this run made. A write does not count towards `max_visits`, and it appears in the run history.

A write takes its model settings the way an agent step in the same place would. Its own `style` comes first, as `{ write = "...", style = "quick" }`, then the styles of the groups holding the action, then the workflow's `style`. So a group is a tidy place to set them once for every write inside it.

Every built-in harness can write a text: Claude Code, Codex, OpenCode and Pi. The write runs on the harness, model, effort and provider its settings resolve to, with the program, credentials and variables you set for that harness. A harness added by a plugin may not write texts; when a write's settings resolve to one, the step fails with a message naming the harness, so give the write a style on a harness that can.

## Before you start

You need a project whose primary folder is a Git repository with a GitHub remote you can push to, Git credentials, and a harness you have signed in to. See [Projects and folders](/projects/).

## 1. Download the example

The one-shot workflow takes a checkout, implements once, reviews once, publishes the change and puts the checkout away. [Download the example](/one-shot.zip) and unpack it into `~/.orbital/workflows/`. Keep every relative path.

![checkout, implement, review. A passing review publishes and cleans up; a rejected review keeps the checkout.](/one-shot.svg?v=5abb76da02)

```toml title="one-shot.toml"
version = 3
entry = "steps.checkout"
description = "Implement one change in an isolated checkout, review it once and publish it"

[inputs.work]
label = "Work"
hint = "The change to make, in plain words or as a ticket URL"

[styles.quick]
effort = "low"

[steps.checkout]
kind = "action"
do = "worktree.create"
next = "steps.implement"

[steps.implement]
style = "implementer"
prompt = "Implement the change described by {{ inputs.work }}. Follow the conventions already in the repository. Do not commit or push."
outputs.implementation_summary = "text"
next = "steps.review"

[steps.review]
style = "reviewer"
prompt = "Review the change in the working folder. The implementer reported: {{ context.implementation_summary }}. Choose pass when the change is complete and correct, or reject when it is not. Do not change files."
outputs.verdict = { kind = "choice", options = ["pass", "reject"] }
routes = [
  { if = "verdict == 'pass'",   to = "steps.publish.commit" },
  { if = "verdict == 'reject'", to = "steps.rejected" },
]

[steps.publish]
style = "quick"

[steps.publish.commit]
kind = "action"
do = "git.commit"
message = { write = "A commit message that says why the change exists" }
next = "steps.publish.push"

[steps.publish.push]
kind = "action"
do = "git.push"
next = "steps.publish.open"

[steps.publish.open]
kind = "action"
do = "github.pr.open"
title = { write = "A short title naming the change" }
body = { write = "Why the change exists, what it changes and how it was checked" }
next = "steps.cleanup"

[steps.cleanup]
kind = "action"
do = "worktree.remove"
routes = [{ if = "git.worktree.clean", to = "steps.published" }]
next = "steps.retained"

[steps.published]
kind = "terminal"

[steps.rejected]
kind = "terminal"
outcome = "failed"
label = "Checkout kept for inspection"

[steps.retained]
kind = "terminal"
outcome = "failed"
label = "Checkout retained to preserve local work"
```

`checkout` takes the isolated checkout. A passing review sends the run into the `publish` group, whose `quick` style sets low effort for every write inside it, so the commit message, title and body each cost a quick model session and no agent turn. `cleanup` removes the checkout and routes on `git.worktree.clean`. Removal refuses when any checkout would lose work, so the run then ends at `retained`, a failed terminal that says what happened.

[Download one-shot.toml](/examples/one-shot/one-shot.toml)

## 2. Validate it

```sh
orbital validate ~/.orbital/workflows/one-shot.toml
```

## 3. Run it

Choose **New task** and pick the project on the repository. Choose the `one-shot` workflow, set **Work** to a description or ticket URL, and write the goal.

A passing review ends with a pull request open on the run's branch and the checkout removed. A rejected review ends the run Failed and keeps the checkout, so you can read the diff.

## Related

- [Action](/reference/step-kinds/#action): every action, its keys and what it records.
- [One-shot implementation](/examples/workflows/one-shot-implementation/): the same workflow, explained step by step.
- [Worktrees and delivery](/projects/worktrees-and-delivery/): checkouts, branches and the pull request loop.
- [Give steps a style](/styles/configure-steps/): the styles a write and a group use.
