Skip to main content
Fixes pair a candidate change with a replay-gate verdict on held-out production evidence. These endpoints mirror the Fixes dashboard and are what the moda fix CLI family calls.

Base URL and authentication

Unlike the rest of the Data API, the fix routes are tenant-scoped and live under the control-plane base:
They accept the same x-api-key header as the Data API (a dashboard session also works). Reads require workspace membership; every mutation (POST) requires the admin role. Responses use camelCase field names — except the fix packet, which is snake_case by design (it is an agent-facing document).

Endpoints

The Fix object

Every fix endpoint (except the packet) returns or embeds this shape:

Candidate refs

candidateRef carries the proposed candidate, discriminated by kind:

Statuses

DRAFT, SCOPING, NO_EVIDENCE, BLOCKED_COVERAGE, UNROUTED, PROPOSING, NO_HEADROOM, PROPOSED, GATING, GATE_FAILED, GATE_INCONCLUSIVE, VERIFIED, PR_OPEN, APPLIED_LOCALLY, HANDED_OFF, SHIPPED, CONFIRMING, HELD, REGRESSED, REJECTED, SUPERSEDED, ABANDONED. The dashboard lifecycle table explains each stage.

Gate result

gateResult is set once a gate finishes:
The verdict reads holdout cases only. pass requires wins − losses ≥ max(2, noiseFloor + 1), losses ≤ noiseFloor, and abstained ≤ 30% of the holdout; more than 30% abstained is inconclusive (never a pass); anything else is fail. verdict values are exactly pass / fail / inconclusive. While the gate is enqueued (GATING), gateResult may instead hold a pending marker — { "pending": true, "baselineFidelity": { ... } } — recorded at enqueue time, before any verdict exists. Treat it as “no verdict yet”; the display surfaces render no gate view for it. The pending marker may also carry enqueue-time counterfactual context (for example the anchors selected) with no counterfactual.verdict — the surfaces render no counterfactual copy for that either.

Counterfactual gate (gateResult.counterfactual)

gateResult.counterfactual is the counterfactual-anchored gate’s verdict: the fix replayed against recorded production failures from holdout traces, two arms per anchor — a control arm without the fix and a candidate arm with it. The block is optional: comparison-only verdicts omit it entirely, and every comparison field above is unchanged whether or not it is present. The verdict math: pass iff wins − losses ≥ max(2, ceil(0.2 × informative)) and informative ≥ 3 and abstained ≤ 30% of anchors; inconclusive when informative < 3 or abstained exceeds 30% (no informative anchors is inconclusive, never a pass); anything else is fail. A counterfactual pass can upgrade a comparison GATE_FAILED/GATE_INCONCLUSIVE fix to VERIFIED — but only when the comparison gate did not fail with losses beyond its noise floor. Overall: pass if either gate passes and neither gate has losses above noise; otherwise the stricter of the two verdicts stands. The top-level reason always names which gate produced the verdict. Display surfaces render the block as prevented {wins} of {informative} replayed production failures (control still failed in {informative}; regressions {losses}) — alongside, never instead of, the comparison verdict. In the fix packet the block rides under the snake_case key counterfactual.

Verdict fidelity

baselineFidelity labels what the baseline arm was made of, so a verdict never claims more than the gate actually compared. It is optional: verdicts produced before fidelity labeling omit it. baselineFidelity.prompt — the prompt pinned in both arms: baselineFidelity.skills — where the baseline arm’s skill content came from: Fidelity is always shown next to Verified. Every surface that renders the verdict — the dashboard badges, the CLI summaries, and the packet — qualifies it: Verified — tenant prompt baseline or Verified — standard-agent baseline, with · skill recovered from production receipts appended when skills is observed. A generic-prompt or recovered-skill verdict never renders as plain “Verified”.

POST /tenants/:tenantId/problems/:problemId/fixes

Drafts a fix. Creation is idempotent while a fix is active: drafting again for the same problem returns the existing active fix instead of forking the pipeline. The new fix rests at DRAFT — drive it with advance.
string
default:"auto"
auto routes from evidence; prompt considers only the prompt route.
A malformed (non-UUID) problem ID returns 400; an unknown problem returns 404. A merged-away problem ID is folded to its survivor, like the problem endpoints.

POST /tenants/:tenantId/fixes/draft-batch

Campaign mode: batch-drafts fixes for the tenant’s top-ranked fixable problems in one call — what the dashboard’s Draft fixes for top problems button and moda fixes draft-batch call. Problems are scanned in rank order; each is drafted through the same guarded creation path as the single-problem route above, or skipped with a reason when it already has an active fix, its latest lifecycle is not open/reopened, or it aliases a problem already handled in the batch. Creation only — nothing advances server-side; drive the drafted fixes with advance, or in bulk with moda fixes drive.
integer
default:"10"
How many fixes to draft, 1–25 (400 beyond the cap). The 25 cap is an LLM spend guard, not a paging nicety: every drafted fix burns distillation + propose budget once driven, so batches are deliberately small — run another after the first lands.
Output (drafted entries trimmed)
drafted entries are full Fix objects decorated with the rank the batch ranked on. Per-problem failures never abort the batch — they land in skipped with the error message as the reason.

GET /tenants/:tenantId/fixes

The ranked queue: fixes ordered by their problem’s rank (descending), then recency. Also available per problem at GET /tenants/:tenantId/problems/:problemId/fixes (returns { "fixes": [...] } without pagination).
string
Filter to one status. An unknown status returns 400.
integer
default:"25"
1–100.
string
Opaque cursor from a previous page’s pagination.next_cursor.
The ranked list scans a bounded window (500 fixes) before ranking and paging, so has_more goes false at the end of that window even when more fixes exist behind it. scan_truncated: true says the tail was cut — page with a status filter to reach the rest rather than treating an empty next_cursor as the whole queue. moda fixes drive surfaces this as a warning.
Reading a fix reconciles its confirmation state lazily: SHIPPED/CONFIRMING fixes are checked against the problem’s lifecycle on every read, so a reopen or 14 clean monitoring days surface as REGRESSED/HELD without a background job.

GET /tenants/:tenantId/fixes/:fixId/packet

The compact agent packet — moda.fix_packet.v1. Every fix has one, including diagnosis-only fixes without a candidate. Quoted evidence in the packet is neutralized as untrusted data before it reaches an agent.
target keys, candidate, and evidence entries are omitted when unknown (for example an UNROUTED fix has only fix_type in target and no candidate). Typed candidates ride the packet under their own keys (injection-screened at propose time and scrubbed again on the way out):
  • candidate_tool (TOOL_SCHEMA fixes) — { tool_name, current_description, current_description_source, proposed_description, param_notes, rationale }: the tool-description diff to apply in your own tool definition.
  • candidate_skill (SKILL fixes) — { skill_name, skill_md }: the complete drafted SKILL.md to install in your agent.
  • candidate_skill_edit (SKILL fixes amending an existing synced skill) — { skill_key, skill_name, current_skill_md, proposed_skill_md, change_summary }: the current-vs-proposed SKILL.md edit to apply to the skill already in your repo.
Apply either one in your stack, then confirm with mark-applied — no GitHub integration required.

POST /tenants/:tenantId/fixes/:fixId/advance

Performs exactly one idempotent pipeline step and returns the updated fix. The backend runs no background jobs — clients drive the pipeline by calling advance in a loop (this is what moda fix … --wait and the dashboard polling loop do). Steps are claimed optimistically, so concurrent advances never double-run a stage; the loser returns the current row untouched.

POST /tenants/:tenantId/fixes/:fixId/verify

Re-gates the fix: enqueues fresh gate and control runs and returns the pinned gate run ID. With candidateContent this gates uploaded local content instead of the stored candidate — the fail-to-pass loop behind moda fix verify --prompt-file. Then drive advance until the fix leaves GATING. For TOOL_SCHEMA and SKILL fixes, both gate arms pin the same prompt so the candidate artifact is the only delta between arms: the tenant’s managed prompt when one exists (baselineFidelity.prompt: "tenant"), or Moda’s fixed standard-agent baseline prompt in both arms otherwise ("generic"). Skill candidates — net-new drafts and edits alike — are registered as content-addressed gate skills automatically: the candidate body rides the proposed arm, and a skill_edit pins the current body (synced or receipt-recovered) in the baseline arm. Only a candidate without gateable content rests at PROPOSED with the gate pending. The resulting verdict always carries its fidelity label.
string
Optional candidate content, 40–200,000 characters, PROMPT fixes only (400 for other fix types). Omitted = re-gate the stored candidate version. Uploaded content is recorded by hash (candidateRef.contentSha256), not registered as a prompt version.
Allowed from PROPOSED, VERIFIED, GATE_FAILED, and GATE_INCONCLUSIVE. A fix already GATING returns 409 (“advance to poll it”), as does any other status.

POST /tenants/:tenantId/fixes/:fixId/submit

Delivers a VERIFIED fix (any other status returns 409).
string
required
pr — Moda’s GitHub App opens a draft PR on branch moda/fix/<shortref> with the composed body and magic word. local_ref — records that you applied the change yourself on a branch (status → APPLIED_LOCALLY).
string
The branch name. Required when channel is local_ref.
Submit enforces the throttles: at most 3 open Moda fix PRs per workspace (409), one open fix PR per problem and per target file (409 names the sibling), and older drafts for the same problem or file are superseded. The pr channel requires a routed target file and a registered candidate version (400 otherwise — content verified via upload must be handed back as a PR by the agent itself); an unconfigured GitHub integration returns 503.

POST /tenants/:tenantId/fixes/:fixId/mark-applied

The no-GitHub confirmation: you applied the candidate in your own infrastructure — updated the tool description in your agent’s tool definition, installed the drafted skill, or hand-applied a prompt edit — so there is no PR merge to confirm the fix. The fix flips straight to SHIPPED with deliveryChannel: "local", records fixRef: { appliedNote, appliedAt, promptVersionId }, and posts the same deterministic mark_fixed problem feedback the merge webhook would, with actor: "fix-mark-applied" and a fix_ref of { fix_id, short_ref, applied_note? } (no pr_url / merge_commit).
string
Optional operator note, ≤ 600 characters, recorded on fix_ref.applied_note.
Returns the updated fix plus a duplicate boolean. Allowed from any resting or delivered status (UNROUTED, PROPOSED, VERIFIED, GATE_FAILED, GATE_INCONCLUSIVE, PR_OPEN, APPLIED_LOCALLY, HANDED_OFF); pipeline-live (SCOPING, PROPOSING, GATING), pre-candidate, and terminal statuses return 409. Idempotent: marking an already-confirmed fix (SHIPPED, CONFIRMING, HELD, REGRESSED) is a duplicate no-op that still re-posts the deterministic feedback. Confirmation monitoring then behaves exactly as after a merge — CONFIRMING while the problem is in fixed-monitoring, HELD after 14 clean days from appliedAt, REGRESSED on reopen.

POST /tenants/:tenantId/fixes/:fixId/dismiss

Rejects the fix (status → REJECTED) and records the reason as problem feedback. Already-terminal fixes (REJECTED, SUPERSEDED, ABANDONED, HELD) return 409.
string
required
1–2,000 characters. Feeds the problem discovery loop.

Fix provenance on problem feedback (fix_ref)

When a PR whose body or head branch carries MODA-FIX-<SHORTREF> is merged, Moda’s GitHub App notifies the backend, which flips the fix to SHIPPED and posts a mark_fixed problem feedback with actor: "moda-github-app" and a structured fix_ref:
mark-applied posts the same feedback shape with actor: "fix-mark-applied" and a fix_ref of { fix_id, short_ref, applied_note? } — no pr_url or merge_commit, because no PR exists. POST /problems/:id/feedback accepts the same optional fix_ref object on any mark_fixed you submit yourself. Notes:
  • fix_ref is stored on the feedback row and threaded into the problem’s lifecycle change reason for provenance; it is not part of the deterministic feedback_id, so re-asserting the same action with an amended ref converges on one row instead of minting duplicates.
  • If a problem that was marked fixed with a fix_ref later reopens automatically, the associated SHIPPED/CONFIRMING fixes flip to REGRESSED.
  • The merge notification is idempotent — redelivered webhook events converge on the same SHIPPED fix and the same pending feedback row.

Errors

Next steps

  • Dashboard: Fixes — the same pipeline with the stepper cards and gate scoreboard.
  • CLI reference: Fixesmoda fixes / moda fix, including the exit-code contract for agents.
  • Problems — the evidence and feedback endpoints fixes are built on.