Run the Full Cycle with Codex CLI
In this tutorial, we will wire Sortie to a Jira project and the OpenAI Codex CLI, then watch the full automation cycle: Sortie picks up a Jira issue, clones your repository, launches Codex to write and commit code, pushes the result to a branch, and transitions the issue to Done. No manual intervention required.
The Jira integration tutorial proved that Sortie can talk to your tracker. This tutorial completes the setup with three new pieces: the Codex CLI agent adapter, workspace hooks for git operations, and a prompt template that guides the agent through the task.
Prerequisites
Jira integration tutorial completed - Sortie connects to your Jira project, and the environment variables
SORTIE_JIRA_ENDPOINTandSORTIE_JIRA_API_KEYare setCodex CLI installed on your machine:
codex --versionYou should see a version string like
0.121.0. If the command is not found, install the Codex CLI. The binary is a statically linked Rust executable with no runtime dependencies.CODEX_API_KEYset in your environment:export CODEX_API_KEY="sk-..."This is a standard OpenAI API key. Codex CLI uses it to authenticate with the OpenAI API, billed at API rates. The adapter checks for this variable when spawning the app-server subprocess and passes it through to the child process.
A git repository on GitHub or GitLab that you can push to
SSH key or HTTPS token configured for
git pushfrom your machine - test it:git ls-remote git@github.com:yourorg/yourrepo.git HEADYou should see a commit hash. If you get a permission error, fix your SSH or token setup before continuing.
Create a Jira issue
Open your Jira project and create an issue that a coding agent can complete without human judgment. We need a task with a clear, verifiable outcome.
Create the issue with these details:
Summary: Create a health check endpoint
Description:
Add a
/healthzendpoint to the project that returns HTTP 200 with the JSON body{"status": "ok"}. Create the filehealthz.go(or the equivalent for the project’s language) with a handler function and register the route. Include a basic test.Status: To Do
Label:
agent-ready
Write down the issue identifier (e.g., PROJ-55). We will see it in the logs later.
The description matters. A real agent reads it as its primary instruction. Vague descriptions like “improve the API” produce vague results. Concrete, verifiable tasks work best with any coding agent.
Set up the project directory
Create a directory for this tutorial:
mkdir sortie-codex-e2e && cd sortie-codex-e2eWrite the workflow file
Create WORKFLOW.md with the full configuration. Replace PROJ with your Jira project key and the git clone URL with your repository:
---
tracker:
kind: jira
endpoint: $SORTIE_JIRA_ENDPOINT
api_key: $SORTIE_JIRA_API_KEY
project: PROJ
query_filter: "labels = 'agent-ready'"
active_states:
- To Do
handoff_state: Done
terminal_states:
- Done
polling:
interval_ms: 30000
workspace:
root: ./workspaces
hooks:
after_create: |
git clone --depth 1 git@github.com:yourorg/yourrepo.git .
before_run: |
git fetch origin main
git checkout -B "sortie/${SORTIE_ISSUE_IDENTIFIER}" origin/main
after_run: |
git add -A
git diff --cached --quiet || \
git commit -m "sortie(${SORTIE_ISSUE_IDENTIFIER}): automated changes"
git push origin "sortie/${SORTIE_ISSUE_IDENTIFIER}" --force-with-lease
timeout_ms: 120000
agent:
kind: codex
command: codex app-server
max_turns: 3
turn_timeout_ms: 3600000
max_concurrent_agents: 1
codex:
model: o3
effort: medium
approval_policy: never
thread_sandbox: workspaceWrite
server:
port: 8080
---
You are a senior engineer working in this repository.
## Task
**{{ .issue.identifier }}**: {{ .issue.title }}
{{ if .issue.description }}
### Description
{{ .issue.description }}
{{ end }}
{{ if .issue.url }}
**Ticket:** {{ .issue.url }}
{{ end }}
## Rules
1. Read existing code before writing anything new.
2. Keep changes minimal — implement exactly what the task requires.
3. Run any available lint and test commands before finishing.
{{ if not .run.is_continuation }}
## First run
Start by understanding the codebase structure. Check for existing patterns
(routing setup, test conventions) and follow them. Write the implementation,
add a test, and verify everything passes.
{{ end }}
{{ if .run.is_continuation }}
## Continuation (turn {{ .run.turn_number }}/{{ .run.max_turns }})
You are resuming. Run `git status` and check test output to understand the
current state. Continue from where the previous turn left off.
{{ end }}
{{ if and .attempt (not .run.is_continuation) }}
## Retry — attempt {{ .attempt }}
A previous attempt failed. Review workspace state and error output before
making changes. Do not repeat the same approach that failed.
{{ end }}This is a lot of configuration in one file. Let’s walk through the pieces, starting with the sections that are new compared to the Jira integration tutorial.
Workspace and hooks
workspace.root: ./workspaces tells Sortie to create per-issue workspace directories under ./workspaces/ relative to your WORKFLOW.md. Each issue gets its own subdirectory named after the issue identifier (e.g., workspaces/PROJ-55/).
Three hooks automate git operations at different lifecycle points:
after_create runs once, when the workspace directory is first created. We clone the repository into it. The . at the end of git clone tells git to clone into the current directory, which is the workspace. --depth 1 fetches only the latest commit for speed.
before_run runs before every agent attempt. It fetches the latest code from main and creates (or resets) a branch named sortie/PROJ-55. On the first run, this creates the branch. On a retry, it resets the branch to a clean state.
after_run runs after every agent attempt. It stages all changes, commits them if there are any, and pushes the branch. --force-with-lease is safe for automation: it pushes only if nobody else modified the remote branch.
Hooks receive environment variables from the orchestrator. We use SORTIE_ISSUE_IDENTIFIER to name the branch. The full set of hook variables:
| Variable | Example | Description |
|---|---|---|
SORTIE_ISSUE_ID | 10042 | Tracker-internal ID |
SORTIE_ISSUE_IDENTIFIER | PROJ-55 | Human-readable ticket key |
SORTIE_WORKSPACE | /home/you/sortie-codex-e2e/workspaces/PROJ-55 | Absolute workspace path |
SORTIE_ATTEMPT | 0 | Current attempt number |
timeout_ms: 120000 gives hooks two minutes to finish. The default is 60 seconds, but cloning a large repository can take longer.
Agent configuration
Two sections control the agent, and they have different scopes.
The agent section configures the orchestrator’s scheduling behavior:
kind: codexselects the Codex CLI adapter.command: codex app-servertells the adapter to launch the Codex app-server, a persistent subprocess that communicates via JSON-RPC 2.0 over stdin and stdout. The subprocess is launched once when the session starts and stays alive across all turns, maintaining full conversation history in memory.max_turns: 3controls how many times Sortie invokes the agent per session. After each turn, Sortie re-checks the issue state in Jira. If the issue moved to a terminal state, the session ends.turn_timeout_ms: 3600000gives each turn up to one hour.max_concurrent_agents: 1runs one agent at a time, which is enough for this tutorial.
The codex extension block
The codex: section is adapter-specific pass-through configuration forwarded to the app-server. Four fields are set here:
model: o3selects the OpenAI model. Replace this with your preferred model.effort: mediumcontrols the reasoning effort level. Options arelow,medium, andhigh. Higher effort produces more thorough work at the cost of more tokens and time.approval_policy: neverauto-approves all tool calls, file edits, and command execution. Required for unattended operation. Without this, the app-server sends approval requests to the client, which stalls the session in headless mode.thread_sandbox: workspaceWriterestricts file writes to the workspace directory and disables network access by default. The adapter setswritableRootsto the workspace path automatically.
For the full list of codex.* fields, see the Codex adapter reference.
Notice that the codex: section has no inner turn budget field. Other adapters (Claude Code, Copilot CLI) have a field that limits how many internal steps the agent takes within a single Sortie turn. The Codex app-server manages its own step execution, working until it completes the task, encounters an error, or hits the turn timeout. With agent.max_turns: 3 and a one-hour turn timeout, the agent has up to three invocations of unrestricted length to finish the job. For a tutorial task like adding a health check endpoint, one turn is usually enough.
Authentication: Codex and Jira
Two credentials are involved, and they serve different systems:
SORTIE_JIRA_API_KEYauthenticates Sortie to the Jira API. This is the Jira token you set up in the Jira integration tutorial.CODEX_API_KEYauthenticates the Codex CLI to the OpenAI API. This is a separate credential with separate billing.
The two tokens have no relationship. You need both set in your environment for the full cycle to work.
Prompt template
The body after the closing --- is a Go text/template rendered per issue. Template variables like {{ .issue.identifier }} are filled with data from Jira.
The prompt branches on three conditions:
- First run (
not .run.is_continuation) tells the agent to read the codebase first, then implement. - Continuation (
.run.is_continuation) means the agent is resuming in the same session. It should check workspace state and continue. - Retry (
.attemptis nonzero and not a continuation) means a previous attempt failed. The agent should diagnose before acting.
The template is agent-agnostic. The same prompt works with Claude Code, Copilot CLI, or Codex. For more advanced templating (conditionals, iteration, custom functions), see Write a prompt template.
Validate the configuration
Check for syntax errors before running:
sortie validate ./WORKFLOW.mdNo output means no errors. Confirm with:
echo $?This should print 0.
Run Sortie
Start Sortie:
sortie ./WORKFLOW.mdYou should see output similar to this (timestamps and IDs will differ):
level=INFO msg="sortie starting" version=0.x.x workflow_path=/home/you/sortie-codex-e2e/WORKFLOW.md
level=INFO msg="database path resolved" db_path=/home/you/sortie-codex-e2e/.sortie.db
level=INFO msg="http server listening" address=127.0.0.1:8080
level=INFO msg="sortie started"
level=INFO msg="tick completed" candidates=1 dispatched=1 running=1 retrying=0
level=INFO msg="workspace created" issue_id=10042 issue_identifier=PROJ-55
level=INFO msg="hook started" hook=after_create issue_identifier=PROJ-55
level=INFO msg="hook completed" hook=after_create issue_identifier=PROJ-55
level=INFO msg="hook started" hook=before_run issue_identifier=PROJ-55
level=INFO msg="hook completed" hook=before_run issue_identifier=PROJ-55
level=INFO msg="workspace prepared" issue_id=10042 issue_identifier=PROJ-55 workspace=…/workspaces/PROJ-55
level=INFO msg="agent session started" issue_id=10042 issue_identifier=PROJ-55 session_id=…
level=INFO msg="turn started" issue_id=10042 issue_identifier=PROJ-55 turn_number=1 max_turns=3The agent is now working. Sortie launched the Codex app-server subprocess, completed the JSON-RPC initialization handshake, authenticated with your CODEX_API_KEY, started a thread, and sent the first turn with the rendered prompt. A Codex session typically takes 3 to 15 minutes depending on the task complexity, the model, and your connection speed. Each agent action (reading files, writing code, running commands) appears as events in the log at debug level.
When the agent finishes a turn, you will see:
level=INFO msg="turn completed" issue_id=10042 issue_identifier=PROJ-55 turn_number=1 max_turns=3
level=INFO msg="hook started" hook=after_run issue_identifier=PROJ-55
level=INFO msg="hook completed" hook=after_run issue_identifier=PROJ-55
level=INFO msg="worker exiting" issue_id=10042 issue_identifier=PROJ-55 exit_kind=normal turns_completed=1
level=INFO msg="handoff transition succeeded, releasing claim" issue_id=10042 issue_identifier=PROJ-55 handoff_state=Done
level=INFO msg="tick completed" candidates=0 dispatched=0 running=0 retrying=0Here is the full lifecycle, step by step:
- Sortie polled Jira and found
PROJ-55in “To Do” with theagent-readylabel. after_createcloned the repository intoworkspaces/PROJ-55/.before_runcreated the branchsortie/PROJ-55fromorigin/main.- Sortie launched
codex app-server, initialized the JSON-RPC session, and started a thread. - The Codex agent read the codebase, wrote an implementation, ran tests, and completed the turn.
after_runcommitted the changes and pushed the branch.- Sortie transitioned the Jira issue from “To Do” to “Done.”
- The next poll found zero candidates and went idle.
Press Ctrl+C to stop Sortie.
Verify the results
Three things should be visible now: the code in the workspace, the branch on your remote, and the issue state in Jira.
Check the workspace
Look at the git log in the workspace directory:
cd workspaces/PROJ-55
git log --oneline -5You should see the agent’s commit at the top:
a1b2c3d sortie(PROJ-55): automated changes
f4e5d6c (origin/main) Initial commitCheck what the agent produced:
git diff HEAD~1 --statThis shows the files the agent created or modified.
Check the remote branch
Back in any directory, verify the branch exists on your remote:
git ls-remote git@github.com:yourorg/yourrepo.git "refs/heads/sortie/PROJ-55"You should see a commit hash. The sortie/PROJ-55 branch is on your remote, ready for a pull request.
Check Jira
Open the issue in your browser. The status should read “Done.” If you use a board view, the card has moved to the Done column.
If the status did not change and you see a handoff warning in the logs, the Jira workflow does not allow a direct transition from “To Do” to “Done.” Check the Jira integration tutorial troubleshooting section for how to resolve this.
Check the dashboard
Open http://127.0.0.1:8080/ in a browser. You will see summary cards (running sessions, retry queue, free slots, total tokens consumed) and a run history table showing the completed session with its issue identifier, turn count, duration, exit status, and token usage.
What we built
We ran the complete Sortie lifecycle with the Codex CLI:
- Poll - Sortie watched Jira for issues matching the
agent-readylabel. - Clone - The
after_createhook cloned the repository into a per-issue workspace. - Branch - The
before_runhook created a clean feature branch. - Code - Codex read the codebase, wrote an implementation, and ran tests.
- Push - The
after_runhook committed and pushed the changes. - Handoff - Sortie transitioned the Jira issue to Done.
Sortie’s adapter-agnostic design means swapping the agent is a config change. The same hooks, prompt template, and orchestration flow work with any supported adapter. To see this same loop with a different agent, try the Claude Code tutorial or the Copilot CLI tutorial.
Where to go next:
- Write a prompt template - conditionals, iteration, and template functions for production prompts
- WORKFLOW.md configuration reference - every field, every default, every constraint
- Monitor with logs - understand the structured log output during long-running sessions
- Monitor with Prometheus - token usage, session counts, and retry rates as time-series metrics
- Codex adapter reference - pass-through configuration, event stream, and error handling
- Scale agents with SSH - remote execution for production workloads
Was this page helpful?