How to Troubleshoot Common Failures
Each section below covers one failure: the log line you see, why it happens, and what to do. For the full error catalog with every error kind and retry formula, see the error reference.
Agent won’t start
level=ERROR msg="worker run failed, non-retryable, releasing claim" error="agent: agent_not_found: agent command \"claude\" not found: exec: \"claude\": executable file not found in $PATH"The agent binary isn’t installed or isn’t on PATH.
Check whether the binary exists:
which claudeIf it’s installed under a different name or path, set
agent.command:agent: kind: claude-code command: /usr/local/bin/claude-codeFor SSH workers, the binary must exist on every remote host. A host missing it fails differently: the run is retried, and its error reads
port_exit: the agent runtime exited before responding: exit status 127followed by the remote shell’s own message (see agent exits before it responds). Check the host directly:ssh build01.internal "which claude && echo ok"Confirm the fix:
sortie validate ./WORKFLOW.md
Agent crashes on authentication
level=WARN msg="worker run failed, scheduling retry" error="agent session start: agent: credential_unverified: the agent runtime did not complete a credential verification request: Not logged in · Please run /login" next_attempt=1 delay_ms=10000Workers never reach a real turn. Before it starts work on an issue, Sortie opens a short-lived session of its own and sends one request through the configured agent, to confirm the credential actually works; see credential verification. Sortie skips that session while it holds a proof of the same agent setup from the last 15 minutes, so a credential that fails right after a good run is refused on the first working turn instead, with the same error kind. What that gives you is the failure happening immediately, before any real work starts, under a name that says it is a credential problem (credential_unverified) rather than a puzzling stall or a working session that quietly fails partway through its first turn.
The message ends with what the agent itself reported, here Claude Code’s answer to a missing ANTHROPIC_API_KEY, but it does not name the variable that is missing or wrong. The steps below find it.
Verify the variable is set:
echo "${ANTHROPIC_API_KEY:-(unset)}"For AWS Bedrock or Google Vertex AI, verify all required variables are set. See environment variables reference for the full list.
Read the
agent stderrwarnings immediately above the error. When a session or a turn fails, Sortie re-emits what the agent wrote to its standard error at WARN, so the runtime’s own auth message is already in the default log.--log-level debugadds every stderr line as it is read, including from turns that did not fail.For SSH workers, check what the launch carried. Credential verification runs against the remote host the same way it runs locally. A remote agent gets the build host’s environment, plus its agent kind’s credential variables and whatever
worker.ssh_pass_envnames, read from Sortie’s own environment. A name Sortie cannot supply is reported at startup:level=WARN msg="ssh_pass_env variable is not set or empty in the orchestrator environment" variable=ANTHROPIC_API_KEYOn
copilot-cli, a rejected credential can instead fail withport_exitand the CLI’s own message, because that CLI refuses a credential by printing a message and exiting. That case is agent exits before it responds; the fixes above still apply.
No agent credential verified line precedes an error raised by the check; that line only appears once the check has actually passed. If you do see agent credential verified followed by a failure, the credential itself is fine and the problem is in the working turn instead: see agent exits without producing output or a turn runs long and gets cut off.
A remote agent authenticates as the wrong account
The session starts and turns run, but the work lands under an identity nobody expected: a pull request opened by the tracker’s service account, or a Copilot session billed to the wrong seat.
A variable Sortie carries overrides the value or login the remote host holds under that name. The usual cause is one variable serving two purposes. tracker.api_key: $GITHUB_TOKEN puts GITHUB_TOKEN in Sortie’s environment for the tracker; copilot-cli declares GITHUB_TOKEN as one of its credential variables; every remote session then signs in with the tracker’s token instead of the host’s own copilot auth login.
Check which names your agent kind carries by default in the environment reference.
Stop Sortie sending the one that collides:
extensions: worker: ssh_hosts: - "build01.internal" ssh_disallow_pass_env: - GITHUB_TOKENSave the file. The worker block reloads without a restart, and the next dispatch runs under whatever identity the host itself holds.
A variable your own ssh_config lists under SendEnv is forwarded by the SSH client itself, out of Sortie’s environment, and ssh_disallow_pass_env does not stop it. Remove the SendEnv entry if that is the path it took.
A remote launch fails before the agent runs
level=WARN msg="agent stderr" line="sortie: dd is required on the remote host to receive environment variables"Sortie delivers environment variables to a remote agent on the SSH session’s standard input, and the remote shell reads them with dd. A host without dd on PATH fails the launch, and Sortie retries it, so the same line repeats until the utility is there.
Every remote opencode launch carries variables whatever your configuration says, because the adapter sends settings of its own. So does every launch that delivers Sortie’s tools, because the tool server’s bearer travels the same way. Other launches carry variables whenever the kind’s credential variables are set in Sortie’s environment or worker.ssh_pass_env names something.
Check each host:
ssh build01.internal "command -v dd && echo ok"Install it where it is missing. It ships with coreutils and with busybox, so most base images have it; distroless and scratch-based images often do not.
A remote agent runs without Sortie’s tools
The agent on an SSH host never mentions tracker_api or the other tools, and the log has this line:
level=WARN msg="declared tool server gap for session" component=tool-relay reason=forward_refusedThe session works, but without Sortie’s tools. reason names the cause.
- For
forward_refused, the host’ssshdrefused the reverse forward Sortie requests.forward_unconfirmedmeanssshreported neither an allocated port nor a refusal, and the same check applies. Run the hand check from let remote agents use Sortie’s tools. With OpenSSH, setAllowTcpForwarding yesorremotefor the SSH user, or addport-forwardingbesiderestricton the key inauthorized_keys. - For
relay_unavailable, the tool server process did not start on the orchestrator host. The log line carries anerroror anexit_code. Runsortie mcp-server --helpas the user running Sortie to confirm the binary starts. - For
runtime_without_http, the runtime accepts no HTTP MCP servers, so it cannot use a tool server that lives on another host. - For
no_connection, the runtime started but never connected withinread_timeout_ms. The runtime may have dropped the declared server; for Kiro CLI, see which login delivers Sortie’s tools.
Each remote session start also opens one short extra SSH connection to learn whether the host accepts the forward. A host that limits new connections per user or per source address sees that extra connection for every session start.
Every reason, and what Sortie does in each case, is in when a session runs without the tools.
Agent exits before it responds
level=WARN msg="worker run failed, scheduling retry" error="agent session start: agent: port_exit: the agent runtime exited before responding: exit status 2: error: unknown option '--sortie-unknown-switch'" next_attempt=1 delay_ms=10000The agent process ended before it answered, and everything after the exit status is the end of what it wrote to standard error, with known credentials shown as [redacted]. A prefix of agent session start: means it happened before the first turn, on the credential check or while the working session started; agent turn N: means a working turn. The quoted output usually names the cause, so read it first. For the exact message format, see the early exit report.
A switch the agent does not accept. Remove or correct the flag in
agent.command. Oncopilot-cli, a CLI older than 1.0.51 rejects the--session-idswitch Sortie always passes; checkcopilot --versionand upgrade. See session identity.A rejected credential. Copilot CLI can report a rejected credential this way rather than as
credential_unverified. Fix it as described under agent crashes on authentication.A program that cannot start. A wrapper script that starts its interpreter through
/usr/bin/envon a host without that interpreter, or a remote host without the agent binary (exit status 127). Install what is missing, or pointagent.commandat a program that exists. A localcopilot-clilaunch fails earlier instead, at its version check: the error readsagent_not_found: copilot binary found but not functional; ensure Node.js 22+ is available, and Sortie releases the claim rather than retrying. See session start.Confirm the fix. Run the configured command by hand, on the same host and in the workspace directory, and check it no longer prints the error. The run is retried with exponential backoff, so the fix takes effect on the next scheduled retry, and
agent credential verifiedin the log shows the check has passed.
Agent exits without producing output
level=WARN msg="agent exited without producing output, treating as failure"
level=WARN msg="worker run failed, scheduling retry" error="agent turn 1: agent: turn_failed: agent exited without producing output: no message from the agent and no tool call" next_attempt=1 delay_ms=10000The agent subprocess wrote output and exited with code 0, but reported no turn outcome, and the adapter found no evidence the model produced anything. Evidence is a message from the agent or a tool call, read from that turn’s own stream, and the error line names the signals the adapter looked for after a colon. Claude Code, Copilot CLI, and OpenCode look for both, so their line ends no message from the agent and no tool call.
Sortie treats every one of these as turn_failed and retries with exponential backoff. An agent that exited without writing anything to standard output fails as agent exits before it responds instead, with its own standard error in the message. Common causes:
MCP config parsing failure. The agent failed to parse
--additional-mcp-configor--mcp-configand exited silently. Check the WARN-level log lines immediately above the error. Sortie emits the agent’s stderr content, which contains the parse error. Oncodexandopencodea bad MCP configuration fails differently: those adapters read it themselves before the agent starts, so the session ends with aresponse_errornaming the file rather than a silent exit.Missing or invalid model configuration. The agent started but the configured model was unavailable, causing an immediate exit before any LLM work.
Rate limiting during initialization. The agent hit an API rate limit before producing any output.
Run with --log-level debug to see the full subprocess stderr. Fix the root cause (correct the config path, set the right API key, wait for rate limits to clear) and Sortie’s exponential backoff retries will succeed automatically.
The agent is killed from outside
level=WARN msg="worker run failed, scheduling retry" issue_id="PROJ-42" issue_identifier="PROJ-42" session_id="session-abc-001" error="agent turn 3: agent: port_exit: exit code -1: signal: killed" next_attempt=1 delay_ms=10000The agent process ended on a signal Sortie did not send, so the turn failed with port_exit and the error text names the signal. Sortie retries it on the usual exponential backoff.
A signal Sortie did send, from stall detection, reconciliation, or shutdown, reports turn_cancelled instead; what happens to the claim afterward is in the worker exit kinds table. An agent killed before it wrote anything takes the early exit report described in agent exits before it responds, whose status reads signal: killed.
Look for what else on the host sends signals. On Linux,
signal: killedwith no one at the keyboard is most often the kernel’s out-of-memory killer. Checkdmesgorjournalctl -kfor anOut of memory: Killed processline near the failure time.Check container and service limits. A memory limit on the container, or a cgroup limit on the systemd unit that runs Sortie, ends the agent the same way. See how to run Sortie as a systemd service and how to deploy Sortie to Kubernetes.
Rule out other tools. A cleanup job or a process supervisor that terminates stray processes by name can match the agent binary.
Copilot CLI stops without finishing the task
level=WARN msg="copilot turn ended without a task-completion report" autopilot_continuations_observed=50 max_autopilot_continues=50
level=WARN msg="worker run failed, scheduling retry" error="agent turn 1: agent: turn_incomplete: agent stopped without reporting the task complete: raise copilot-cli.max_autopilot_continues if the turn needs more steps" next_attempt=1 delay_ms=10000Copilot CLI exited cleanly, but the turn stopped before it produced a session.task_complete report: the --max-autopilot-continues ceiling was reached mid-task. Only the Copilot CLI adapter can report this; every other agent adapter still has no way to detect a turn cut off partway through.
Raise
copilot-cli.max_autopilot_continues(default50) if the task genuinely needs more autopilot steps per turn. Seeagent.max_turnsvs.copilot-cli.max_autopilot_continuesfor how this budget relates toagent.max_turns.Let the retry run.
turn_incompleteis retried on the same exponential backoff as other transient turn failures, and the retry starts a new Copilot session over the same workspace, so the files the earlier session wrote are still there. Raising the ceiling is often enough on its own.Tell it apart from a stall or a genuine failure. A stall reports
turn_cancelled; an agent that reported its own failure, or exited with nothing to show for the turn, reportsturn_failed.turn_incompletemeans specifically that the turn’sresultevent arrived and the exit was clean, but nosession.task_completereport ever did.
A turn runs long and gets cut off
level=WARN msg="turn timeout exceeded" issue_id="PROJ-42" issue_identifier="PROJ-42" session_id="session-abc-001" turn_timeout_ms=1800000 turn_number=2
level=WARN msg="worker run failed, scheduling retry" issue_id="PROJ-42" issue_identifier="PROJ-42" session_id="session-abc-001" error="agent turn 2: agent: turn_timeout: turn exceeded the configured 1800000 ms bound; the adapter's own report follows: context deadline exceeded" next_attempt=1 delay_ms=10000The turn ran longer than agent.turn_timeout_ms. The attempt fails and is retried on the usual exponential backoff; it is not abandoned.
Tell it apart from a stall. A turn timeout reports
turn_timeout; a stall reportsturn_cancelledand fires on silence rather than duration, regardless of how long the turn has been running. See the error reference for both error kinds.Set a larger value if the task is genuinely long-running. See how to configure retry behavior for the tradeoffs between a longer turn timeout and the stall-detection ratio.
A run stops because it needs a person
level=WARN msg="agent asked for a decision only a person can make, ending the attempt"
level=ERROR msg="worker run failed, non-retryable, releasing claim" error="agent turn 2: agent: turn_input_required: agent asked for a decision only a person can make: an answer to a question"The agent asked for something no unattended run can supply: an answer to a question, or a permission the runtime gave Sortie no way to refuse and still continue. Sortie refuses rather than consenting on your behalf, and ends the attempt rather than waiting. The claim is released, no retry is scheduled, and the run is recorded with status needs_person rather than failed.
Read the message tail, not just the error kind. The text after
turn_input_required:names what the agent asked for.an answer to a questionandwider filesystem or network accessare the two tails in use today.Do not reconfigure the agent for non-interactive mode. It already is. Every runtime is launched in a mode that cannot ask interactively, and a pass-through setting that would undo that is refused before the run starts. This ending happens on the paths that survive that launch posture.
Give the agent what it lacked, outside the run. If it wanted wider access, widen the sandbox: for Codex,
codex.thread_sandboxandcodex.turn_sandbox_policy. If it asked a question, the answer belongs in the issue or in the prompt template, so the next dispatch does not need to ask. See the Codex adapter reference for which requests end an attempt and which ones the agent can work around.Narrow the task if neither applies. An issue whose resolution genuinely needs a human decision is not work an unattended agent can finish, and repeated
needs_personruns on the same issue are the signal to take it out of the dispatch set.
A session is stopped in flight by the token budget
level=WARN msg="run stopped by token ceiling" issue_id="PROJ-42" issue_identifier="PROJ-42" session_id="session-abc-002" reason=token_budget used_tokens=1503417 budget_tokens=1500000 issue_tokens_completed=1481200 session_tokens=22217 sum_source=confirmed_read ceiling_setting=agent.max_tokens unmeasured_sessions=0 unaccounted_turns=0The issue’s cumulative token spend reached agent.max_tokens while a session was running, so Sortie cancelled the worker rather than let the session run to the end over budget. The attempt is recorded with status budget_stopped, the claim is released, and no retry is scheduled.
If the stop landed while the run was in self-review, or before that phase started, the run is recorded the same way and makes no handoff transition, even where the phase would otherwise have passed. At the next poll tick the issue enters the budget-exhausted set, and that is what posts the comment naming the ceiling on the tracker.
Tell it apart from a stall or a reconciliation kill. Those cancel the same worker context, so the agent reports the same
turn_cancellederror either way. The recorded status is what separates them: only the token ceiling recordsbudget_stopped. See the worker exit kinds table.Read
session_tokensagainstissue_tokens_completedbefore changing anything.session_tokensis what the cancelled session had spent;issue_tokens_completedis what the issue’s earlier sessions had already banked. When the second figure sits just under the budget on its own, the session had almost no room from the start, and raising the ceiling buys the issue another attempt rather than a longer one. Whensession_tokenscarries most of the total, one session is spending the whole budget andagent.max_turnsor the adapter’s own per-turn cap is the tighter lever.Raise the ceiling only if the work is worth it.
agent.max_tokensreloads from WORKFLOW.md without a restart, and the new value reaches the sessions already running from the next poll tick. See how to control agent costs for choosing a figure.Set a warning threshold so the agent wraps up next time instead of getting cut off.
agent.token_warning_percentlogstoken warning threshold reachedand marks thecost_budgettool’swarning_reachedfield before the ceiling stops a run, giving a well-prompted agent room to finish cleanly. See how to control agent costs.
Issue keeps re-running and never advances
level=WARN msg="handoff withheld by evidence policy" issue_id="PROJ-42" issue_identifier="PROJ-42" policy="observed" verdict="absence of work observed" reason="workspace commit and working tree match the run baseline" turns_completed=2 consecutive_absences=1Sortie’s default tracker.handoff_evidence policy withholds the handoff transition when a run leaves the workspace exactly as it found it. The issue stays in its active tracker state, and Sortie retries it on exponential backoff rather than failing silently or moving it forward on the strength of an exit code alone.
Check what the agent actually did. A withheld run states its verdict as the run’s
errorin the run history API:The agent finished without changing anything, so Sortie did not hand the issue off.when the workspace matched the baseline, orSortie could not tell whether the agent changed anything, so it did not hand the issue off.when Sortie could not measure it. If the agent reported success but changed nothing in the workspace, that is exactly the case this policy exists to catch, unless the requested outcome had genuinely already held, which is the next case.A run that finds nothing to change should say so. If the issue’s outcome already held before the agent started, the agent can write
no-change-neededto.sortie/statusinstead of leaving the workspace silently unchanged. Sortie appends this instruction to every first-turn prompt automatically, so no prompt template change is needed. A declaration that survives self-review (where enabled) is never withheld and does not count toward the ceiling in item 3 below. It records the run as succeeded and moves the issue totracker.no_change_stateif one is configured. See state machine reference: declaring that nothing needed changing. A run that changes nothing and declares nothing keeps the withheld outcome described above.A dispatch whose only product is a tracker write is a known false positive. If your agent’s entire job is calling
tracker_apito transition the issue itself, settracker.handoff_evidence: off. See workflow configuration.Repeated absences park the issue. After a bounded number of consecutive withheld runs, Sortie stops retrying and applies an escalation label instead of looping forever. See park issues stuck in a loop of empty runs.
Not every workspace can be measured. A workspace that is not a Git work tree changes nothing under the default policy. Only
strictwithholds there, and it withholds every transition. See the state machine reference.
Tracker returns 401 or 403
level=ERROR msg="failed to fetch candidate issues" error="tracker: tracker_auth_error: HTTP 401: Unauthorized"The API token is wrong, expired, or lacks required permissions. Sortie does not stop polling on this error. It logs it and retries on the next poll interval, so you will see it repeat until you fix the credential.
Verify the environment variable resolves to a non-empty value:
echo "${SORTIE_JIRA_API_KEY:-(unset)}"Test the token directly:
curl -s -u "you@company.com:your-api-token" \ "https://yourcompany.atlassian.net/rest/api/3/myself" | head -5That is the pairing Sortie sends for a key in
email:tokenform. A Data Center personal access token carries no colon and goes out as a bearer credential instead.If you use
handoff_state,in_progress_state, or atracker_commentnotifications entry, the token needs to be able to write to issues, not only read them.
Sortie won’t start: endpoint is rejected
level=ERROR msg="failed to construct tracker adapter" error="tracker: tracker_payload_error: gitea: endpoint \"https://gitea.example.com:abc\" is not a valid absolute http(s) url"endpoint failed to parse as an absolute http or https URL carrying a hostname. This is a construction-time failure on every adapter that reads an endpoint (GitHub, Gitea, GitLab, Linear). Sortie refuses to start rather than letting a bad value reach the HTTP client and fail later as a network error. The same check runs offline through sortie validate, except for a Gitea CI or SCM endpoint set through a top-level gitea: override, which validate does not inspect.
Three shapes commonly trigger this:
- A port with no host, such as
http://:8080. Give the hostname:http://gitea.internal:8080. - An unbracketed IPv6 address, such as
http://fd00::1:3000, the exact form an address prints as fromip addr. Add brackets around the address:http://[fd00::1]:3000. - A query string or fragment appended to the base URL, such as
https://gitlab.example.com?insecure=1. Remove it; the adapter appends its own API path and has nowhere to put one.
If the endpoint carries a username or password, the error message masks it before printing, so a credential never appears in the log.
Template render fails
level=ERROR msg="template render error in WORKFLOW.md (line 24): can't evaluate field titel in type map[string]any"Sortie runs templates in strict mode. Unknown variables are hard errors. Three common causes:
Typo in a field name. Check the name against the variable table. The error message names the exact field and line.
Unguarded nil field.
.issue.parentisnilwhen no parent exists. Wrap it:{{ if .issue.parent }}{{ .issue.parent.identifier }}{{ end }}Dot rebinding inside
range. Inside{{ range .issue.labels }},.is the current element. Use{{ $.issue.identifier }}to reach the root.
Run sortie validate ./WORKFLOW.md after every template edit to catch these before runtime.
Workspace won’t create
level=ERROR msg="workspace create: permission denied: /opt/sortie_workspaces/PROJ-42"Three variants:
Permission denied. The process user can’t write to
workspace.root. Fix permissions or change the root to a writable path like~/sortie-workspaces.Containment violation (
path escapes root). An issue identifier produced a path outside the workspace root (a security boundary). Investigate the identifiers in your tracker.Disk full. Check with
df -h /opt/sortie_workspaces.
Hook script fails
level=WARN msg="after_create hook failed, rolling back workspace" issue_id=abc123 issue_identifier=PROJ-42 workspace=/opt/sortie_workspaces/PROJ-42 error="hook run: exit_code=128: exit status 128" hook_output="Cloning into '.'...\nfatal: Could not read from remote repository."A hook exited non-zero. after_create and before_run failures are fatal for the attempt; after_run and before_remove are logged but ignored. For a fatal hook the orchestrator follows up with a worker run failed, scheduling retry line carrying the same error.
Read the
hook_outputattribute on the WARN record. It holds the hook’s combined stdout and stderr at every log level; no debug flag is needed. The value keeps the last 8 KiB of output and starts with a truncation marker when earlier output was dropped. A hook that printed nothing produces nohook_outputattribute. Output of successful hooks appears only at--log-level debug, onhook completedrecords.Test the hook manually:
mkdir /tmp/test-ws && cd /tmp/test-ws git clone --depth 1 git@github.com:acme/backend.git .Common causes: SSH key not forwarded, wrong repo URL, missing dependencies. Hooks run with a restricted environment that strips variables like
GIT_SSH_COMMAND; see the environment reference.For timeout errors, increase
hooks.timeout_msin WORKFLOW.md.
Workspace path is a symbolic link
level=WARN msg="worker run failed, scheduling retry" error="mcp config generation: open .sortie directory: /opt/sortie_workspaces/PROJ-42: is a symbolic link" next_attempt=1 delay_ms=10000Something replaced the per-issue workspace directory with a symbolic link during workspace preparation, after Sortie confirmed the directory was real but before it wrote .sortie/mcp.json: an after_create or before_run hook script, or a process racing that window from outside Sortie. A symlink left behind by an earlier run’s after_run or before_remove hook is caught earlier instead, as a workspace conflict when this attempt’s workspace preparation starts; see step 2.
Find what replaced the directory. Check
after_createandbefore_runfirst; a script that removes and relinks a shared directory is the most common cause. Also check for an outside process (a cleanup job, a manualln -s) that could run during that same window.Remove the symlink by hand. Workspace preparation on the next attempt only recreates a directory that is missing; a directory still occupied by a symlink is rejected instead, as a workspace conflict, so retries keep failing, under a different error, until the link is gone.
If the workspace directory disappears entirely instead of being swapped, at any point from a hook through a later turn, Sortie reports it as an agent error instead:
level=ERROR msg="worker run failed, non-retryable, releasing claim" error="agent session start: agent: invalid_workspace_cwd: workspace path does not exist: /opt/sortie_workspaces/PROJ-42: lstat /opt/sortie_workspaces/PROJ-42: no such file or directory"This one releases the claim instead of retrying with backoff. The issue becomes dispatchable again on the next poll cycle, and that dispatch’s own workspace preparation recreates the missing directory from scratch.
A hook that starts after the same directory breaks fails with its own hook validate error instead of either of the above; see hook errors in the error reference. See control-file errors, agent errors, and path errors for the full set of detail strings these checks can report.
Issues not being dispatched
level=INFO msg="tick completed" candidates=0 dispatched=0 ... running=0 retrying=0 ...(the tick completed line carries more fields than shown here; see How to monitor Sortie with logs for the full field list)
Sortie is polling but finds nothing to dispatch. At the default log level this line appears only when its counters change, so an idle Sortie does not repeat it every poll. Run with --log-level debug to see every tick.
First check whether Sortie holds the issue back on purpose. Open Overview and read the Not dispatched block, or list the held issues from the state endpoint:
curl -s localhost:7678/api/v1/state | jq '.held[] | {issue_identifier, why: .hold.reason_text, release: .hold.release_text}'An issue listed there is eligible. Its entry says what holds it and what releases it, so the capacity, blocker, and spend ceiling steps below only explain the cause and the remedy. The list is empty after a restart until a poll tick has run. An issue that is not listed is either not a candidate, which the other steps below check, or already tracked elsewhere, such as running, retrying, parked, or between stages.
State names must match exactly. Verify
tracker.active_statesmatches your tracker (case-sensitive)."To Do"and"to do"are different states.An open issue carries no state label (GitHub, Gitea, GitLab). Sortie dispatches an open issue only when it carries a label from
tracker.active_states. It logsskipped open issues without a state labelat INFO with the number it left alone, and logs again only when that number changes. A label fromactive_statesthat the tracker does not have draws the warningconfigured active label missing from tracker labelsat the first poll. An open issue that also carries aterminal_stateslabel reads as terminal, because terminal labels outrank active ones.Use dry-run to see what Sortie would dispatch:
sortie --dry-run ./WORKFLOW.mdEach candidate gets a
would_dispatchorskip_reasonfield in the log.Concurrency cap reached. If
runningequalsagent.max_concurrent_agents, new issues wait, and Not dispatched shows them as “No free agent slot: 3 of 3 running”. A limit fromagent.max_concurrent_agents_by_statereads “No free agent slot for In Progress: 2 of 2 running” and holds only issues in that state. With SSH hosts configured, “No free SSH host” means every host runs as many sessions asmax_concurrent_agents_per_hostallows. Increase the cap or wait for a running agent to finish.Query filter too narrow. A typo in
tracker.query_filterreturns zero results. Use--dry-run --log-level debugto see the full query.A blocker hasn’t cleared, or its list couldn’t be read. Not dispatched reads “Blocked by PROJ-12 (In Review)” while a blocker is open, and “Blockers not read: a tracker request failed”, “Blockers not read on this poll”, or “Blocker list incomplete” when Sortie could not check them. Dry-run output carries the same cause as a
skip_reason:blocked_bymeans a listed blocker hasn’t reached a terminal state yet.blockers_unresolvedandblockers_not_readmean Sortie couldn’t read the blocker list this poll (a failed read, or the per-poll read budget was already spent on other candidates) and will retry on a later poll. This applies to GitHub and Gitea, which read dependencies separately from the candidate list. See candidate eligibility for the full gate, andsortie_candidate_holds_totalon the Prometheus metrics reference to watch this over time instead of one poll at a time.A per-issue budget ceiling was reached. An issue held by
agent.max_sessions,agent.max_tokens, oragent.max_cost_usdstays in its active tracker state and is skipped on every poll. CheckGET /api/v1/{identifier}forstatus: "budget_exhausted", the budget item under Needs attention on the dashboard’s Overview, or grep your logs forblocking re-dispatch. Sortie also posts one comment on the issue naming the ceiling that stopped it, and that comment counts the issue’s sessions a ceiling stopped in flight, if any. See how to control agent costs.A retry keeps being rescheduled with
retry agent settings refused. The retry queue shows that text as the issue’s error. The settings the retry would run with, a dispatch rule’s block laid over the agent kind’s block, fail a check that is an error, usually after an edit toWORKFLOW.md. Runsortie validateto see the same check, then fix the settings and the next retry starts. Thecheckanddiagnosticfields of the log record name the failure; see dispatch and agent settings.The issue is parked. A parked issue stays in its active tracker state and is skipped on every poll until a person releases it. Check the
parkedlist ofGET /api/v1/state, orGET /api/v1/{identifier}forstatus: "parked"; the entry names the reason and the parking label. See release a parked issue for how a park lifts.The running configuration is not the file you edited. A reload that fails validation keeps the previous configuration in force. Sortie logs the failure and reports it in the
healthblock of the state endpoint:curl -s localhost:7678/api/v1/state | jq '.health.workflow'prints the error, the time of the last successful load, and how many loads have failed since. Astatusoffailmeans the process still runs the older file;/readyzkeeps passing in that case. Runsortie validateon the file, fix the error it reports, and the next reload clears the failure. To see what the process applies instead, runcurl -s localhost:7678/api/v1/state | jq '.config.tracker.active_states, [.config.dispatch.rules[].name]'. See the HTTP API reference for everyhealthandconfigmember.The instance spend ceiling was reached. With
agent.max_daily_cost_usd,agent.max_weekly_cost_usd, oragent.max_monthly_cost_usdset, Sortie starts no agent session for any issue once the estimated cost of the UTC day, the UTC week (which starts on Monday at 00:00 UTC), or the UTC month reaches that window’s ceiling, and scheduled retries wait with their attempt numbers kept. Every eligible issue shows under Not dispatched with the reason “Daily spend ceiling reached”, “Weekly spend ceiling reached”, or “Monthly spend ceiling reached” and the time left until the window resets. Check the item of the same name under Needs attention on the dashboard,curl -s localhost:7678/api/v1/state | jq '.spend_ceiling'forholds_dispatchandresets_at, or grep your logs forspend ceiling reached. Dispatch stays held until every window that has reached its ceiling clears, either because the window resets or because you raise or remove its ceiling. See how to control agent costs.An agent provider’s usage limit is reached. Only
claude-codeandcodexproduce this hold; on any other agent kind a refused session takes the ordinary retry backoff and no issue is held. When the provider refuses a session and reports when the limit resets, Not dispatched shows every issue routed to that kind with the reason “Usage limit reached for” the kind and the time left until the reset. A limit on one configured model reads “Usage limit reached for” that model “on” the kind, and issues routed to other agents or models keep dispatching. To confirm, list the entries withusage_limitas the reason:curl -s localhost:7678/api/v1/state | jq '.held[] | select(.hold.reason == "usage_limit") | {issue_identifier, agent_kind: .hold.agent_kind, resets_at: .hold.resets_at}'The issue whose session the provider refused is not in that list; it waits in the retry queue. Nothing in
WORKFLOW.mdlifts the hold. Wait forresets_at, or route the issues to another agent kind with a dispatch rule. How the wait works is in wait for a provider usage limit; the fields are in the hold object.Sortie could not read the comments since a park. After a person releases a parked issue, the next run receives the comments posted since the park. When the read of those comments fails, Not dispatched reads “Waiting to read the comments since the park”,
hold.reasonispark_comments_unread, and the log carriespark release comment read failed, holding issue. The issue is no longer parked. The next poll reads the comments again and dispatches the issue once the read succeeds; a read that keeps failing points at the tracker connection; see Tracker returns 401 or 403.
Sortie won’t start at all
dispatch preflight failed: tracker.kind is requiredSortie validates the config at startup and reports all failures at once. Run sortie validate ./WORKFLOW.md to see every problem, including advisory warnings for typos in YAML keys and type mismatches that would silently fall back to defaults at runtime. The most common missing fields:
| Field | Required by |
|---|---|
tracker.kind | Always |
tracker.project | Jira adapter |
tracker.api_key | Jira adapter (after $VAR expansion) |
active_states or terminal_states | At least one non-empty |
terminal_states | GitHub, Gitea, and GitLab adapters: at least one entry |
If $VAR references aren’t resolving, verify the variables are exported in the shell that runs Sortie:
env | grep SORTIESee the workflow configuration reference for every field, default, and constraint.
Was this page helpful?