Skip to content
Orbital

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.

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"

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.

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 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:

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 gives every key.

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.

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.

The one-shot workflow takes a checkout, implements once, reviews once, publishes the change and puts the checkout away. Download the example 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.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

Terminal window
orbital validate ~/.orbital/workflows/one-shot.toml

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.