Remediations
A diagnosis ends in a fix you can review
Once a cause is confirmed, Decim writes the solution: an SQL repair for a person to run, a repository patch checked to apply cleanly, both, or a plain statement that no fix is needed. One click opens a draft pull request on GitHub. Nothing is merged automatically.
Why a diagnosis is not enough
Knowing the cause and knowing what to do about it are separated by a surprising amount of careful work.
The sample investigation, from the product's demo workspace, ends with a confirmed cause.
Tuesday's release, commit 9f3c2e1, replaced a hard-coded currency list in the
stored procedure stg.usp_load_transactions with a parameter,
@Currencies, defaulting to 'USD,GBP'. The nightly job calls the
procedure without the parameter. Every EUR row, about 41,000 a night, has been filtered out of
staging since, and the step has reported success with zero errors every time. The
investigations page shows how that was established.
For the engineer who picks this up, the diagnosis is the start of the second half. The procedure has to change, and so, arguably, does the job that relies on its default. Four nights of EUR rows are sitting in the raw export, marked as loaded, and need to be loaded again without duplicating anything already in staging. The reload has to happen after the fix is deployed, not before. Someone has to confirm it worked, and someone has to tell finance which figures were wrong and when they will be right.
Each of those steps is a chance to cause a second incident. A solution from Decim does not make those decisions better than your team would. It makes sure the mechanical parts arrive written, scoped and in order, and that the decisions arrive marked as decisions.
Five kinds of solution
Not every cause is fixed in code, and not every incident needs a fix at all. A solution says which case this is.
There is a pull towards making every fix a diff, because diffs are familiar and reviewable. It produces nonsense: a pull request that claims to reload rows it cannot touch, or a change to code that was never the problem. So a solution takes one of five forms, and the form tells you who does what.
| Kind | Contains | What Decim does |
|---|---|---|
| SQL repair | A diagnostic SELECT, one bounded UPDATE, a regression SELECT | Writes it. A person on your team runs it |
| Repository patch | A diff against the repository, checked to apply cleanly | Opens it as a draft pull request on one click. Never builds or runs it |
| Both | A patch to stop the damage and an SQL repair to undo it | Delivers each in its own way, with the order stated |
| No fix required | A statement that nothing needs changing, and why | Records it, so nobody fixes what was not broken |
| Blocked | The inputs the solution still needs | Lists them, instead of guessing |
The sample incident needs both: a patch to stop EUR rows being filtered from tonight onwards, and an SQL repair to restore the four nights already lost. The rest of this page walks through each.
Anatomy of an SQL repair
Three statements, always in the same shape. Two read. One changes data, and it is bounded.
Data repairs go wrong in predictable ways: an UPDATE with a WHERE
clause slightly wider than intended, a reload that duplicates rows already present, a fix
applied before the code that caused the damage is gone. The three-part shape is there to make
each of those visible before anyone runs anything.
- A diagnostic
SELECTmeasures the damage first. If its result does not match what the diagnosis predicts, stop: the repair is aimed at the wrong thing. - One bounded
UPDATEmakes the change. Bounded means explicit dates, an explicit scope, and an expected row count stated beside it, so an unexpected count is caught rather than committed. - A regression
SELECTproves the repair worked, by comparing the repaired data with an independent reference.
Decim writes the repair. It never runs it: Decim does not connect to your databases. A person on your team reads it, runs it where it belongs, and decides when. For the sample incident the repair is below. Table and column names come from the sample repository.
-- 1. Diagnostic (read-only). EUR rows per night: export vs staging,
-- from three nights before the release to the night before the fix.
SELECT e.business_date,
COUNT(*) AS eur_in_export,
COUNT(s.entry_id) AS eur_in_staging
FROM raw.ledger_entries AS e
LEFT JOIN stg_transactions AS s ON s.entry_id = e.entry_id
WHERE e.currency = 'EUR'
AND e.business_date >= '2026-10-10'
AND e.business_date < '2026-10-17'
GROUP BY e.business_date
ORDER BY e.business_date;
-- Expected: counts match (about 41,000) up to 2026-10-12,
-- then eur_in_staging = 0 from 2026-10-13, Tuesday. -- 2. The one bounded change. Run only after the patch in PR #482 is deployed.
-- Tuesday's version marked every row of the night as loaded, EUR included,
-- so the skipped EUR rows are no longer 'new' and no run will pick them up.
-- This returns them to the queue: four nights, one currency, nothing else.
BEGIN TRANSACTION;
UPDATE e
SET load_state = 'new'
FROM raw.ledger_entries AS e
WHERE e.currency = 'EUR'
AND e.business_date >= '2026-10-13'
AND e.business_date < '2026-10-17'
AND e.load_state = 'done'
AND NOT EXISTS (SELECT 1 FROM stg_transactions AS s
WHERE s.entry_id = e.entry_id);
-- Expect roughly 4 x 41,000 rows. Anything far outside that range: stop.
IF @@ROWCOUNT NOT BETWEEN 150000 AND 175000
ROLLBACK TRANSACTION;
ELSE
COMMIT TRANSACTION; -- 3. Regression (read-only). Run after the next nightly load.
-- Every night since the release should now stage every EUR row it exported.
SELECT e.business_date,
COUNT(*) AS eur_in_export,
COUNT(s.entry_id) AS eur_in_staging,
COUNT(*) - COUNT(s.entry_id) AS still_missing
FROM raw.ledger_entries AS e
LEFT JOIN stg_transactions AS s ON s.entry_id = e.entry_id
WHERE e.currency = 'EUR'
AND e.business_date >= '2026-10-13'
GROUP BY e.business_date
ORDER BY e.business_date;
-- Expected: still_missing = 0 on every row. The regression check compares staging with the export, not with itself. A check that asks "are there EUR rows in staging now" would pass after a partial reload. A check that asks "is every exported EUR row staged" cannot.
The repository patch
A diff against the code discovery already tied to the failing step, checked to apply cleanly before anyone sees it.
Because pipeline discovery has already linked the
stg_transactions step to sql/stg/usp_load_transactions.sql and to the
job spec that calls it, the patch is written against the exact lines the investigation cited.
Before it is offered, Decim checks that it applies cleanly to the repository. It does not
build, compile or run it: that is what your CI and your reviewers are for.
diff --git a/sql/stg/usp_load_transactions.sql b/sql/stg/usp_load_transactions.sql
--- a/sql/stg/usp_load_transactions.sql
+++ b/sql/stg/usp_load_transactions.sql
@@ -1,5 +1,5 @@
CREATE OR ALTER PROCEDURE stg.usp_load_transactions
- @Currencies NVARCHAR(200) = 'USD,GBP'
+ @Currencies NVARCHAR(200) = 'USD,GBP,EUR'
AS
BEGIN
SET NOCOUNT ON;
diff --git a/jobs/ledger-nightly.yml b/jobs/ledger-nightly.yml
--- a/jobs/ledger-nightly.yml
+++ b/jobs/ledger-nightly.yml
@@ -5,6 +5,6 @@ steps:
- name: extract-ledger
run: python -m ledger_etl.extract --date "{{ run_date }}"
- name: stage-transactions
- sql: EXEC stg.usp_load_transactions
+ sql: EXEC stg.usp_load_transactions @Currencies = 'USD,GBP,EUR'
- name: convert-currency
sql: EXEC fx.usp_convert_transactions The diff is small, which is typical. Most pipeline incidents resolve to a few lines; the difficulty was never the typing. It changes two things, for different reasons. Restoring the default puts the procedure back to the behaviour it had before Tuesday. Passing the list explicitly from the job step means the next change to the default cannot silently drop a currency again, because the job no longer depends on it.
Whether to keep a default at all is a design question for your team. Removing it would make every caller state its currencies, which is safer and breaks any caller that does not. The patch takes the conservative option and leaves that decision visible in review.
One click to a draft pull request
The patch becomes a branch and a draft pull request on GitHub, through the GitHub App, on a repository you selected.
From the solution, one click opens the pull request. It opens as a draft, on its own branch, in the repository the patch belongs to. Your checks run on it exactly as they would on a branch pushed by a colleague, and it waits for a person to review it, mark it ready and merge it. Nothing is merged automatically, at any confidence.
In the sample workspace the result is draft pull request #482, "Restore EUR in the ledger currency filter". Its description carries the reasoning, so a reviewer can check why the change is right without opening Decim.
Restore EUR in the ledger currency filter (#482, draft)
## What this changes
- stg.usp_load_transactions: @Currencies defaults to 'USD,GBP,EUR' again.
- jobs/ledger-nightly.yml: the stage-transactions step passes the list
explicitly, so the job no longer depends on the default.
## Why
Commit 9f3c2e1 (Tuesday's release) replaced the hard-coded list
('USD','GBP','EUR') with @Currencies, defaulting to 'USD,GBP'. The job calls
the procedure without the parameter, so every EUR row has been filtered out
of staging since 2026-10-13, while the step reported success with 0 errors.
## Evidence
- ledger-nightly / step 3, 2026-10-13 02:14:09: rows loaded for USD (57911)
and GBP (12206), no EUR line. Previous night: 40903 rows for EUR.
- Row counts by currency before and after Tuesday (read-only check, run by
the team): EUR drops to zero on Tuesday; USD and GBP are steady.
- The core banking export still carries about 41,000 EUR rows a night.
## Not in this PR
The four affected nights need reloading. That is a separate SQL repair,
to be run by a person after this change is deployed. Note the last section. The pull request fixes the code and says plainly what it does not do. The data repair is not hidden inside a migration that runs on deploy; it stays a separate artefact for a person to run, in the order the callout above describes.
No fix required, and blocked
Two outcomes that are easy to skip, and both are worth recording.
No fix required
Some incidents need no change. An upstream file arrived late once and the next run caught up; a dashboard was read before the night's load finished; the "missing" rows were filtered on purpose by a rule nobody remembered. A solution that says no fix is required, with the reason, is a result. It stops a well-meant change to code that was working, and it records why the incident closed.
Blocked
Sometimes Decim knows the cause but cannot write a responsible fix. The code involved may be in a repository that is not connected; the deciding check may not have been run yet; the cause may be clear enough while the affected range is not. In those cases the solution is blocked, and it lists the inputs it still needs. A blocked solution that names what is missing is more useful than a confident patch built on a guess.
What Decim never does
The boundary is structural, not a setting: there is no path from Decim into your databases or deployments.
Every solution ends with a person. That is partly about safety and mostly about knowledge: the people who run the pipeline know things no evidence trail contains, such as a freeze before month-end, a downstream consumer that cannot take a reload mid-morning, or a finance team that needs a warning before figures change.
| Action | Automated? | Detail |
|---|---|---|
| Run an SQL repair or any query | Never | Decim has no database connection. A person runs every statement |
| Build, test or execute a patch | Never | It checks the patch applies cleanly; your CI and reviewers do the rest |
| Merge a pull request | Never | Pull requests open as drafts. A person reviews and merges |
| Deploy, restart or rerun a job | Never | There is no execution path into your environment |
| Open a draft pull request | On one click | Through the GitHub App, on a repository you selected |
What stays a human decision
Confidence in the diagnosis is not confidence in the fix. They are different quantities.
In the sample, the diagnosis was confirmed at 92%, corroborated by the log lines, the commit diff and a row count the team ran. That is strong evidence about what happened. It says nothing about whether the reload should run tonight or after month-end close, whether the finance dashboard for the affected days needs a correction notice, or whether the procedure should keep a default at all.
A good solution makes those questions explicit instead of answering them quietly. That is the real argument for draft-by-default. It is not that the patch might be wrong, though review catches that too. It is that a fix carries business decisions the evidence cannot settle.
The board report
One click exports the incident as a PDF for people who will never open Decim, including the remediation.
An incident like the missing EUR totals does not stay with engineering. Finance wants to know which numbers were wrong and when they will be right; a manager wants to know whether it can happen again. The PDF board report answers both from the same record the engineers worked from, in this order.
| Section | What it holds |
|---|---|
| Executive summary | What happened, the impact, and the outcome, in a page a non-engineer can read |
| Investigation timeline | Each phase, from locating the pipeline to the conclusion |
| Root cause | The cause, with its confidence and the outcome: cause found, likely cause, cause unclear, or needs log access |
| Evidence | The cited code, commits, log lines and ticket content behind the conclusion |
| Verification | How the cause was settled, including any check a person ran and its result |
| Remediation | The solution: its kind, the patch and SQL repair where there are any, and what is left for a person |
| Appendix | Supporting detail for anyone who wants to check the working |
The remediation section is where the solution reaches a wider audience: the kind of fix, the patch and SQL repair where there are any, and what is still waiting for a person. Because it sits after the evidence and the verification, a reader can see that the fix follows from a checked cause rather than from a guess.
An incident still in progress exports as a clearly marked interim report, so you can brief people before the investigation concludes without presenting a provisional answer as final. The same record is also available as JSON for your own tooling.
Common questions
Will Decim merge anything automatically?
Does Decim run the SQL repair?
SELECT, one bounded UPDATE and a regression SELECT, and a person on your team runs them where and when they choose.Is the patch tested before the pull request opens?
What permissions does opening a pull request need?
We do not use GitHub. Can we still use the fix?
What happens if we disagree with the diagnosis after seeing the fix?
Related: investigations for how the diagnosis is reached; evidence for how log lines and commits are cited; pipeline topology for how the failing step was tied to its code; and security for what Decim can and cannot access.
Get started
Bring us a pipeline that broke last week
The fastest way to evaluate this is a real incident you already know the answer to. If Decim gets it wrong, that is a far more useful demo than one where it doesn't.