Scripting

The steps of the loop that a script or a shell prompt wants also run as commands, without the interface: status, reviews, standup, branch, pr and announce, beside doctor and config. This page is what a script can rely on from them — the exit status, which stream carries what, the JSON shapes, and the flags that make a write safe to run unattended. Every command and flag is listed in the command reference.

workflow status --json                 # where the work stands, as data
workflow --dry-run pr                  # what pr would push and open
workflow pr --yes                      # push, open, link and move, without asking
workflow standup --no-edit --yes       # post the day's standup, unattended
workflow --log requests.log doctor --online   # a bug report's evidence

Exit status

A script tells one kind of failure from another by the exit status, never by the message, which is prose and may change.

StatusMeaningFor example
0Success.
1Any other failure.An issue that is not in the tracker, a push that was rejected, git missing, a repository whose branch cannot be read.
2Usage: the command was called wrongly.An unknown flag, command or subcommand; a wrong number of arguments; standup --days 0; a confirmation with no terminal to answer it.
3Configuration: fix the file, a credential or a login.No .workflow.json for doctor or config show; a file that does not parse; a required field left empty, or set unusably; a file other users can read; no messaging configured for announce; no repository remote for reviews to find the forge from; a credential that is missing — a Jira token, or a forge token from the file, the environment or gh auth login — or one a service rejected, a Jira 403 that says what the token may not do among them.
4A refused precondition: the command would not go ahead because of what it found.A pull request already open; no commits to open one for; no pull request to announce; a branch or configuration file that already exists; a directory that is not a git repository.
5Unreachable: a service did not answer, or asked you to wait.Jira, the forge or the messaging service could not be reached, or answered with a redirect — a sign-in gateway in front of it, say — which is refused so the credential goes nowhere else; rate limiting.
130Interrupted by Ctrl+C.

cobra’s own help command is the one exception to 2: asked about a command it does not know, it prints the root’s help and exits 0.

A command that reports several problems at once — doctor checks every section, status DIR… every directory — exits with the first family any of them belongs to, in the order 2, 3, 4, 5, 1. So doctor with no configuration and no git exits 3: the configuration is what to fix first.

A .workflow.json that exists but does not parse stops every command with 3, rather than being replaced by the defaults; one that cannot be opened stops it with 1. With no file at all, the commands that can run on the defaults still do.

With forge.cli set, the forge is asked through gh or glab, and a CLI that is not signed in reads as a forge that could not be reached: 5, not 3. workflow doctor --online says which it is.

The same families on the web

workflow --web answers a failed request with a problem code (see Web API errors). The families line up:

Exit statusWeb problem code
2 usagebad_request (400); precondition_required (428), for a configuration save that names no revision to write over (no If-Match)
3 configurationunprocessable (422), for a missing messaging service, an invalid configuration body, a configuration file on disk that no longer reads as valid, a Jira token not configured or not accepted, or refused with a 403 that says what it may not do, a jira.base_url that is not a usable address, a forge token not found or not accepted, and a forge.kind set without its forge.host
4 refused preconditionconflict (409)
5 unreachableunreachable (502), for a service that could not be reached, answered with a redirect or asked to wait
1 failureinternal (500); the web also answers not_found (404) for a missing issue, unprocessable (422) for any other change Jira refused, a jira.base_url with no Jira API behind it, a forge address with no forge API behind it, or a request the forge refused for the token’s permissions, and unreachable (502) for a status the forge does not document — all of which the command line counts as a plain failure

The web has no problem code of its own for a missing or rejected credential. A Jira or forge credential answers unprocessable and points at workflow doctor, as the command line exits 3 — except a Jira 403 that says what the token may not do, which says Jira refused the request: doctor checks who a token is, not what it may do.

Standard output and standard error

Standard output carries the artifact — what a script would capture: the JSON, the preview of what a write will do, the draft, the rows, and what a write created. Standard error carries everything said about it: dry-run lines, declined and done notices, warnings, guidance, the questions a command asks, and the error itself, prefixed workflow:.

Commandstdoutstderr
statusthe line, or one row per directory, or the JSON
reviewsone line per review, or the JSON“No pull requests are waiting on your review.”
doctorthe report, or the JSON
config showthe configuration as JSON, credentials maskedthe file it came from (# PATH); how to create one when there is none
config initwith --dry-run, the file it would write, as JSON, maskedprogress, the checks, “Wrote …”, what to do next, a warning when the file is not ignored by git
standupthe draft“Nothing to share.”, the dry-run line, “Not posted.”, “Posted to …”
branchBranch NAME from BASE and switch to it, then Created NAMEthe dry-run line, “Not created.”
prOpen TITLE and BRANCH → BASE, then Opened #N URL (!N on GitLab)the dry-run line, “Not opened.”, the offers to link it on the issue and to move the issue to the review status, and their outcomes
announcethe message and where it goesthat an earlier session already announced this moment, the dry-run line, “Not announced.”, “Announced to …”
workflow --webthe address it serves on

So workflow config show | jq . parses, and workflow reviews | wc -l counts reviews and nothing else.

JSON

--json is on the reads: status, reviews and doctor. config show prints JSON always. None of them carries a credential: config show masks each to its last four characters, and the others never print one.

workflow status --json prints one object:

{
  "issue": "PROJ-412",
  "summary": "Fix token redaction",
  "stages": [
    { "name": "Issue", "state": "done" },
    { "name": "Branch", "state": "done" },
    { "name": "Commits", "state": "done" },
    { "name": "Review", "state": "in_flight" },
    { "name": "Slack", "state": "not_started" }
  ],
  "ci": "running"
}

A stage’s state is done, in_flight, failed or not_started; ci is running, passed, failed or none. The last stage is named for the messaging service messaging.kind configures — Slack, Teams, Discord or Webhook — so read the stages by position rather than by that name. That stage reads done once the open pull request was announced at the moment it is at now — ready for review, or CI red — by announce or the interface, as the store remembers it; with store.disabled it never does. issue and summary are left out when the branch names no issue.

workflow status --json DIR… prints an array, one object per directory, each with a repository label. A directory that cannot be read has an error instead of the stages — "not a git repository", or why its repository could not be read — and the command then exits non-zero after printing the whole array.

workflow reviews --json prints an array, oldest first:

[
  {
    "number": 42,
    "title": "fix(config): redact the webhook",
    "author": "ana",
    "repository": "acme/api",
    "draft": false,
    "ci": "passed",
    "age": "3d",
    "url": "https://github.com/acme/api/pull/42"
  }
]

workflow doctor --json prints the report as an object: version; repository (inside_work_tree, root, branch, detached, remote, forge); tooling, one entry per program (name, found, required, effect); configuration (path, jira_url, jira_auth_mode, messaging_target, messaging_mode, world_readable, missing), or config_problem when the file did not load; and credentials (checked, and with --online over a file that loaded, results: service, status — ok, rejected or unreachable — and detail). It exits as the prose report does. A service that answers with a redirect, or asks you to wait, is unreachable and exits 5: it never judged the credential.

workflow config show prints the configuration file’s own shape, as Configuration describes it.

Writing without a person: --yes and --dry-run

branch, pr, announce and standup print a preview and ask before they write. Two flags change that:

  • --yes goes ahead without asking. On pr it answers every question: the push, the open, and the offers that follow it — to link the pull request on the branch’s issue and to move the issue to the review status. A link that fails is said on stderr, the move is still made, and the command exits non-zero. On announce it never repeats an announcement: when the store says this pull request was already announced at the moment it is at, it says so on stderr, announces nothing, and exits 0 — run without --yes to be asked. It does not skip standup’s editor: add --no-edit for that.
  • --dry-run prints the preview and what the command would do, and writes nothing. It is one flag for every command, given before the command’s name or after it: workflow --dry-run pr and workflow pr --dry-run are the same. config init --dry-run runs its checks and prints the file it would write, masked, without writing it or storing anything in the keychain. Bare workflow --dry-run is the interface with every write held back.

A write run without --yes and without a terminal — stdin piped or closed — has no way to be answered, so it stops, says to pass --yes, and exits 2. The one exception is announce at a moment already announced, which --yes would leave as it is: it says to run it at a terminal instead.

A log for a bug report: --log

--log FILE appends a one-line outline of every request a command makes — the time, the service, the method, the path, the status and how long it took — to FILE. Like --dry-run, it is accepted before or after any command. It records nothing else: never a header, a body, a query string or a host, and never the path of a messaging webhook — Slack’s, Teams’, Discord’s or a plain one — which is itself the credential. The file is created readable only by you.

2026-09-22T18:04:11Z jira  GET  /rest/api/2/myself 200 184ms

workflow --log requests.log doctor --online is the run a bug report most wants: every credential check, each under its service’s name.