Investigations

Hypotheses, tested — not a paragraph of plausible prose

An explanation you cannot check is a guess with better formatting. Decim treats each possible cause as a claim to be settled by evidence, and shows its work either way.

Why an explanation is not a diagnosis

Ask any capable model why a pipeline lost rows and it will tell you. That is the problem.

You will get a fluent, well-organised answer listing schema drift, upstream delays, mapping gaps and connection failures. It will be correct in the sense that those are indeed the things that cause this. It will also be indistinguishable from the answer you would get about a pipeline that failed for a completely different reason, because it was produced without looking at yours.

The failure mode is specific: plausibility is not evidence, and prose has no mechanism for telling the two apart. A paragraph that says "the mapping table was likely stale" reads exactly like one that says "the mapping table was stale, here are the 455,382 rows it rejected and the timestamp of the first one." Only one of those survives someone checking.

So the unit of an investigation here is not a paragraph. It is a hypothesis — a specific claim, with a state, tested against something that ran against your systems.

Model

A hypothesis is an object

Five states. The interesting ones are the two that never become an answer.

Each candidate cause carries a state, the evidence that moved it there, and a note on what it would take to settle it. The states are deliberately not a confidence gradient — they describe what the evidence did, which is a different thing from how sure anyone feels.

Hypothesis states
State Meaning Why it exists
proposedGenerated, not yet testedExists so that the set considered is visible, not just the winner
supportedEvidence is consistent with itConsistent is not sufficient — this state never becomes a diagnosis alone
refutedEvidence contradicts itCarries the artefact that killed it, so you can disagree with the reasoning
confirmedEvidence is sufficientForms the diagnosis; must account for the observed magnitude
blockedCannot be testedNames the evidence it would need — a request, not a shrug

The distinction between supported and confirmed is the one that does the most work. A great many wrong diagnoses are hypotheses that were merely consistent with the symptom. Consistency is cheap — on any given day several true statements about your pipeline will be consistent with rows going missing. Sufficiency is the bar: the cause has to account for the size of what happened.

Worked example

A worked investigation

Transaction volume down 38% overnight. Six hypotheses, four refuted, and an arithmetic check that nearly did not close.

What follows is the canonical demonstration incident, with illustrative figures. The shape is what matters: a symptom that could have half a dozen causes, most of which are eliminated cheaply, and one that survives because it accounts for the magnitude.

The symptom. A daily report shows transaction volume at 744,118 against an expected 1,200,000 — down 38%. Every run reported success. No alert fired. The scheduler log is clean, and the on-call engineer slept through the night, correctly, because nothing was broken in any sense a monitor understands.

  1. 1

    H1 — Upstream sent fewer transactions

    Refuted. The obvious first suspect, and the one that would make this somebody else's problem. The manifest shows 48 of 48 expected files received, every one within 2% of its 30-day median row count. Upstream sent a normal day.

    Manifest against the trailing median
    -- H1: did upstream simply send less?
    -- Compare the day's manifest against the trailing median, per file.
    SELECT
        m.FileName,
        m.ReceivedAtUtc,
        m.RowCount,
        med.MedianRowCount,
        CAST(100.0 * m.RowCount / NULLIF(med.MedianRowCount, 0) AS decimal(5,1)) AS PctOfMedian
    FROM dbo.FileManifest AS m
    CROSS APPLY (
        SELECT MedianRowCount = AVG(h.RowCount)
        FROM dbo.FileManifest AS h
        WHERE h.FileName = m.FileName
          AND h.ReceivedAtUtc >= DATEADD(day, -30, m.ReceivedAtUtc)
          AND h.ReceivedAtUtc <  m.ReceivedAtUtc
    ) AS med
    WHERE CONVERT(date, m.ReceivedAtUtc) = '2026-08-05'
    ORDER BY PctOfMedian;
  2. 2

    H2 — A connection failure dropped batches

    Refuted as the cause. There was a connection pool timeout at 02:07:19, and it did cost rows. But one batch is 500 rows against a shortfall of 455,882 — three orders of magnitude short. Kept on the record rather than discarded, because it is real and it still needs accounting for.

  3. 3

    H3 — A schema change truncated or rejected rows

    Refuted. sys.objects.modify_date puts the last alteration of dbo.[Transaction] 94 days before the incident, and no column type or nullability changed in the window. A schema change cannot cause a failure that began at 02:14 today.

    Object and column metadata
    -- H3: did the target schema change under us?
    -- modify_date on the object, plus current column metadata. If the table was
    -- last altered months ago, a schema change did not cause a failure today.
    SELECT
        o.name              AS [table],
        o.modify_date       AS table_last_altered,
        c.name              AS [column],
        t.name              AS data_type,
        c.max_length,
        c.is_nullable
    FROM sys.objects AS o
    JOIN sys.columns AS c ON c.object_id = o.object_id
    JOIN sys.types   AS t ON t.user_type_id = c.user_type_id
    WHERE o.name = 'Transaction' AND o.type = 'U'
    ORDER BY c.column_id;
  4. 4

    H4 — Rows were diverted, not lost

    Confirmed. The reject table holds 455,541 rows for the day, and 455,382 of them share one reason and one value: unmapped PaymentType / CONTACTLESS. First seen 02:14:07, continuing to end of day. The remaining 159 are the ordinary background rate this table shows every day. The rows were never lost — they were routed somewhere nobody watches.

    Reject profile for the day
    -- H4: were rows diverted rather than lost?
    -- Group the reject table by reason and value. A reason that was absent last
    -- week and dominant today is the finding.
    SELECT
        Reason,
        RejectedValue,
        COUNT(*)            AS Rows,
        MIN(RejectedAtUtc)  AS FirstSeen,
        MAX(RejectedAtUtc)  AS LastSeen
    FROM dbo.RejectedTransactions
    WHERE RejectedAtUtc >= '2026-08-05'
      AND RejectedAtUtc <  '2026-08-06'
    GROUP BY Reason, RejectedValue
    ORDER BY Rows DESC;
  5. 5

    H5 — Something changed the set of payment types

    Confirmed. config/tender-map.json was last modified 11 days before the incident and contains no CONTACTLESS key. The upstream point-of-sale release notes for 04 August record contactless payments being reported as a distinct code where they had previously been folded into CREDITCARD. The mapping did not change; what it had to map did.

  6. 6

    H6 — The nightly sync was responsible

    Refuted as the cause, retained as a contributor. The 02:00 sync did exhaust the connection pool and did stall the loader for four minutes. It explains the 02:07 timeout in H2. It does not explain a shortfall that continues for twenty-two hours after the sync completed at 02:11.

The reject query returns the finding directly. One value, appearing for the first time at 02:14 and continuing all day:

Result — reject profile, 05 August text
Reason                    RejectedValue   Rows      FirstSeen             LastSeen
------------------------  --------------  --------  --------------------  --------------------
unmapped PaymentType      CONTACTLESS     455,382   2026-08-05 02:14:07   2026-08-05 23:58:41
unmapped PaymentType      GIFTCARD_V2         118   2026-08-05 09:12:33   2026-08-05 20:44:02
failed FK: StoreId                 —           41   2026-08-05 06:03:19   2026-08-05 22:17:55
Arithmetic

Where the arithmetic closes

The step that separates a diagnosis from a story: add it up, and see whether it comes to the right number.

A cause that explains the symptom qualitatively is not finished. The rows have to add up. Here is the first attempt, and it fails:

Reconciliation — first pass text
received   1,200,000     intake manifest, 48 of 48 files
loaded       744,118     dbo.[Transaction], batches 88101–88644
rejected     455,382     dbo.RejectedTransactions, reason 'unmapped PaymentType'
                 118     dbo.RejectedTransactions, reason 'unmapped PaymentType'
                  41     dbo.RejectedTransactions, reason 'failed FK: StoreId'
timed out        500     batch 88413, connection pool exhausted 02:07:19
                         ---------
                         1,200,159   <-- does not close: 159 rows over

159 rows over. Small enough to wave away, and waving it away is exactly the move that turns an investigation into a narrative. The discrepancy is real and it has a cause: the loader's row-by-row retry path. When batch 88413 timed out, 159 of its 500 rows were retried successfully on the next pass and landed in the target — so they were counted once as timed out and once as loaded.

Reconciliation — with the retry path accounted for text
received   1,200,000
loaded       744,118
rejected     455,541     (455,382 + 118 + 41)
timed out        341     batch 88413 — 159 of the 500 were retried successfully
                         ---------
                         1,200,000   <-- closes

Why the rejected ones matter

Four of the six hypotheses above were refuted. That is not overhead — for most readers it is the more useful half.

Consider what the refutations cost to produce, and what they are worth. "Upstream sent less" is the first thing anyone checks, and checking it properly means finding a manifest, working out the right baseline, and comparing per file. When an investigation hands you that already done, with the query and the numbers, it has removed the most likely wrong turn before you took it.

There is a second, less obvious reason. A diagnosis with its rejections visible is falsifiable by the reader. You can look at H3 and say: that query only checks the target table, and our staging table has its own schema. Now you have found a real gap in the reasoning in ten seconds. A conclusion presented without its alternatives gives you nothing to push against except the conclusion itself, and disagreeing with a confident paragraph is much harder than disagreeing with a specific query.

This is also why refuted keeps its artefact rather than just a verdict. "We ruled out a schema change" is an assertion. "We ruled out a schema change because modify_date is 94 days old, here is the row" is something you can check and, if warranted, overturn.

Limits

When it cannot tell

A blocked investigation names the evidence it needs. It does not fall back to the best available story.

The most dangerous moment in any investigation is the point where the evidence runs out but the pressure to conclude does not. A system optimised to always produce an answer will produce one, and it will be the most plausible remaining candidate — which is precisely the failure described at the top of this page, arriving by a more respectable route.

So blocked is a first-class outcome. It reports which hypotheses could not be settled and what would settle them, which converts a dead end into a specific, actionable request.

Common blockers and what they cost
Missing evidence What cannot be established Consequence
No intake or manifest tableCannot establish a denominatorReconciliation becomes relative, not absolute
Logs rotated before the windowCannot correlate to a batch idRetention silently sets the investigation horizon
No reject or error tableCannot distinguish diverted from lostThe most consequential gap in this category
No source control connectedCannot correlate to a changeTiming evidence remains; attribution does not
Reference table already overwrittenCannot see what the pipeline actually readA truncate-and-reload mapping destroys its own history

Several of these are worth fixing before an incident rather than during one. A reject table with a reason column, a retained intake count, and log retention that outlives your reporting cycle are cheap to add and decide whether a future investigation is possible at all. The topology surfaces which of them you currently have.

Confidence, and what checks it

A number that is only meaningful if something eventually disagrees with it.

Each hypothesis and each diagnosis carries a confidence score. On its own that is close to worthless — a self-reported score is a claim about a claim, and a system with no feedback loop will happily report high confidence forever.

What makes it mean something is the comparison afterwards: what engineers actually confirmed when they closed the incident. A confidence score that is not calibrated against outcomes is decoration, and the honest position while the corpus is small is that calibration is a commitment rather than a track record. Decim is pre-release; there is no accuracy figure to quote here, and inventing one would undermine the argument the rest of this page is making.

Re-investigating

Evidence arrives late. A log shipper catches up, source control gets connected, someone restores a backup of the mapping table as it was that morning. Re-running an investigation against a fuller picture is normal, and a conclusion that does not survive more evidence was never a conclusion. Prior runs are retained so the two can be compared, which is also how the calibration above accumulates.

Common questions

Is this just an LLM summarising our logs?
A model proposes candidate causes and decides what to test next — that part is genuinely well suited to it. What it is not permitted to do is assert a conclusion. Every hypothesis is settled by a query or a log match that runs against your systems and is recorded with its result, so a claim either has an artefact behind it or it does not reach the diagnosis. The reasoning is generated; the findings are measured.
What stops it from producing a confident wrong answer?
Two things, and the second matters more. A hypothesis must account for the observed magnitude, not merely be consistent with the symptom — which is what kept the connection timeout out of the diagnosis above despite being real. And where evidence is missing, the state is blocked and names what it would need, rather than falling back to the most plausible remaining story.
Can we see the hypotheses it rejected?
That is most of the interface. Each refuted hypothesis keeps the evidence that killed it, because the ruling-out is the work you would otherwise repeat by hand — and because it is the only way to disagree with the reasoning rather than just the answer.
How long does an investigation take?
Long enough to run the queries, which is dominated by your systems rather than by ours. The design constraint is not speed: an answer in ninety seconds that nobody trusts costs more than one in ten minutes that closes the incident.
What happens when new evidence arrives?
Re-run it. A conclusion drawn before a log shipper caught up, or before source control was connected, should be re-tested against the fuller picture — and a diagnosis that does not survive more evidence was never a diagnosis. Prior runs are retained so the two can be compared.
Does it need to have seen the failure before?
No. There is no library of known failure signatures to match against. The method is the same regardless of novelty: establish what arrived, reconcile it against what landed, and test each candidate cause against evidence. That is why it works on a bespoke service nobody outside your company has seen.

Related: evidence for how each artefact is collected and cited; pipeline topology for how the graph is assembled; remediations for what happens after a diagnosis; and the write-up of this failure mode in longer form.

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.