Pipeline topology
A pipeline starts with what triggers it
Decim reads a GitHub repository and finds the pipelines in it. Each one is defined by its trigger, drawn as a graph of steps, and tied to the exact lines of code behind every step. It shows what the repository contains, and says plainly what it cannot see.
What discovery reads
One input: a GitHub repository you have selected. Decim reads its code the way an engineer reads an unfamiliar codebase, and writes down the pipelines it finds.
An investigation cannot start from a symptom alone. "EUR totals are missing from the finance dashboard" has to become "this step, in this pipeline, defined by these lines". Discovery is the part of Decim that makes that translation possible, and it does the work before any incident exists, so an investigation starts from a map rather than a blank page.
The input is deliberately narrow. You install the GitHub App, choose the repositories Decim may read, and select one for discovery. Decim reads the code, the SQL files, the job specs and the configuration in it. It does not connect to your databases, schedulers or cloud consoles to do this, and it does not need the pipeline to be running.
There are no format-specific parsers. Decim does not need a pipeline to be written in a particular framework or declared in a particular file. It reads code in any language, follows calls from one file to the next, reads SQL embedded in strings, and resolves a step name in a job spec to the procedure it executes. That is why it works on a Python extract script, a .NET worker, a Java batch job and a directory of stored procedures alike.
How a repository becomes a set of pipelines
Five stages, run when a repository is scanned and again when it is rescanned.
- 1
Install the GitHub App and choose repositories
Access is per repository. Decim reads code, commits and pull requests from the repositories you select, and nothing else in the organisation.
- 2
Find every trigger
Decim reads the repository for the places where work starts: schedules, queue consumers, file watchers, change-feed handlers and batch entry points. Each trigger becomes one pipeline.
The trigger for the Nightly ledger load # jobs/ledger-nightly.yml name: ledger-nightly schedule: "0 2 * * *" # 02:00 UTC, every night steps: - name: extract-ledger run: python -m ledger_etl.extract --date "{{ run_date }}" - name: stage-transactions sql: EXEC stg.usp_load_transactions - name: convert-currency sql: EXEC fx.usp_convert_transactions - 3
Follow each trigger to its steps
From the trigger it follows what the code actually does: the module the job runs, the procedure a step executes, the table that procedure writes, the next step that reads it. Every step records the file and line range it came from.
- 4
Mark connections and points of interest
Each edge between steps is marked direct or inferred, and lines where pipelines tend to break are flagged as points of interest. Both are covered below.
- 5
Match against what is already known
On a rescan, each pipeline found is matched to the one Decim already holds, so its identity, and every incident scoped to it, survives the code changing underneath.
A pipeline is defined by its trigger
Not by a folder, a file name or a diagram. A pipeline is the work that follows from one thing that starts it.
Repositories are organised for the people who write them, not for the people who investigate them. One service can contain three pipelines; one pipeline can span a Python module, two stored procedures and a report query in another folder. Grouping by directory produces a map that matches the code layout and nothing else.
A trigger is unambiguous. Something starts the work: a clock, a message, a file, a change, a
command. Everything that runs because of it, in order, is the pipeline. That definition also
matches how incidents are reported. Nobody says "the ledger_etl package is
broken"; they say "last night's load is short".
| Trigger | In the code, for example | Typical pipeline |
|---|---|---|
| Schedule | A cron expression in a job spec, a crontab line, a Hangfire RecurringJob.AddOrUpdate registration | Nightly ledger load, 02:00 UTC |
| Queue consumer | A handler bound to a queue, topic or subscription | A service that loads each order event as it arrives |
| File arrival | A watcher on a directory, share or bucket prefix | A daily partner file dropped into an inbound folder |
| CDC | A handler on a change feed or change table | Replicating changed customer rows downstream |
| Batch command | An entry point a person or another system runs: a CLI command, a console app | A month-end restatement run by hand |
The middle column is illustration, not a list of patterns Decim searches for. A scheduler Decim has never seen before still schedules something, and the code that registers the job still says when it runs.
Worked example: Nightly ledger load
The sample incident used across this site. It comes from the product's demo workspace, not a customer.
The repository is github.com/acme/ledger-etl. Discovery finds a job spec that runs
at 02:00 UTC every night and becomes the trigger for a pipeline Decim names
Nightly ledger load. The job pulls the core banking ledger's REST export, stages
it with a stored procedure, converts currencies with a second procedure that joins the ECB
daily FX rates, and writes the warehouse table fact_transactions, which the finance
daily report reads. About 111,000 entries go through it a night: roughly 41,000 in EUR, 58,000
in USD and 12,000 in GBP.
# jobs/ledger-nightly.yml
name: ledger-nightly
schedule: "0 2 * * *" # 02:00 UTC, every night
steps:
- name: extract-ledger
run: python -m ledger_etl.extract --date "{{ run_date }}"
- name: stage-transactions
sql: EXEC stg.usp_load_transactions
- name: convert-currency
sql: EXEC fx.usp_convert_transactions From that file, discovery follows each step into the code it runs. The result is a graph of seven nodes, each with its role, the code behind it, and how it connects to the node before.
| Step | Role | Code behind it | Connection in |
|---|---|---|---|
| Core banking ledger | Source | ledger_etl/extract.py L18–27, the REST export URL and request | — |
| Extract ledger | Transformation | ledger_etl/extract.py L31–74, writes raw.ledger_entries | Direct |
| stg_transactions | Transformation (stored procedure) | sql/stg/usp_load_transactions.sql L1–48, called by the job step | Direct |
| FX rates feed (ECB daily rates) | Source | ledger_etl/fx_rates.py L9–40, writes ref.fx_rates | Inferred |
| Currency conversion | Transformation (stored procedure) | sql/fx/usp_convert_transactions.sql L1–51, joins ref.fx_rates | Direct |
| fact_transactions | Destination (warehouse table) | sql/fx/usp_convert_transactions.sql L52–66, the INSERT | Direct |
| Finance daily report | Destination | reports/finance_daily.sql L1–30, selects from fact_transactions | Inferred |
Stored procedures in the repository
In many pipelines the transformation logic is not in the service at all. It is a stored
procedure, and the service, or the job spec, only calls it. A topology that stops at
EXEC stg.usp_load_transactions has stopped exactly where the interesting part
begins.
When the procedure is kept in the repository, Decim traces into it. The job step names the procedure; the procedure's file defines its parameters, the tables it reads and the table it writes. The step in the graph points at those lines, not at the one-line call.
-- sql/stg/usp_load_transactions.sql (as committed in 9f3c2e1)
CREATE OR ALTER PROCEDURE stg.usp_load_transactions
@Currencies NVARCHAR(200) = 'USD,GBP'
AS
BEGIN
SET NOCOUNT ON;
INSERT INTO stg_transactions (entry_id, booked_at, currency, amount)
SELECT e.entry_id, e.booked_at, e.currency, e.amount
FROM raw.ledger_entries AS e
WHERE e.load_state = 'new'
AND e.currency IN (SELECT value FROM STRING_SPLIT(@Currencies, ','));
-- ...per-currency "Loaded N rows for <currency>" log lines,
-- then "Step completed successfully (0 errors)"
END
In the sample incident this is the file that matters. Tuesday's release, commit
9f3c2e1, replaced a hard-coded list ('USD','GBP','EUR') with the
@Currencies parameter, defaulting to 'USD,GBP'. The job step calls the
procedure without the parameter, so every EUR row is filtered out, and the step still reports
success with zero errors. Because discovery had already tied the step to these lines, the
investigation could cite them directly. The
investigations page follows that diagnosis end to end.
Direct and inferred connections
Every edge in the graph says how it was established. There are two answers, and the difference matters during an incident.
A direct connection is one the code states outright. The job spec executes
stg.usp_load_transactions by name. The extract module writes
raw.ledger_entries and the procedure reads it. The conversion procedure inserts into
fact_transactions. There is a line of code for each of these, and the edge cites it.
An inferred connection rests on something weaker: a shared table name, a
convention, a configuration value. In the example, the FX rates loader writes
ref.fx_rates and the conversion procedure joins it, but nothing in the repository
calls one from the other or orders them. They meet only at the table. The finance report is
similar: a SQL file in reports/ selects from fact_transactions, and
nothing in the pipeline runs it.
Inferred edges are usually right. They are marked anyway, because the cases where they are wrong are the cases that cause incidents. When the FX file arrives late, the timing between the loader and the conversion is the question, and an edge the code never enforced is exactly where to look. That was hypothesis (B) in the sample investigation. It was rejected: the FX file had landed before conversion started.
Points of interest
Lines where pipelines tend to break, flagged during discovery so an investigation knows where to look first.
Most code in a pipeline moves rows from one place to another and is rarely the problem. A small number of lines decide which rows move at all. Discovery flags these as points of interest on the step they belong to. Typical examples:
- a filter on a list of values, hard-coded or parameterised, like the currency list above
- a parameter with a default that callers rely on without passing it
- a join that silently drops rows with no match, such as a rates or mapping lookup
- a date window computed from the clock, which shifts when a run is late or rerun
- an error handler that logs and continues, or a success message written regardless of row count
A point of interest is not a finding, and most of them never cause an incident. It is a place a careful engineer would read first when rows go missing, marked in advance so nobody has to rediscover it at night.
Identity across rescans
Code changes every week. The pipeline it implements should not become a new pipeline every time it does.
When a repository is rescanned, Decim matches each pipeline it finds to the one it already holds. A renamed module, a split procedure or a new step changes the graph of the existing pipeline; it does not create a second pipeline beside the first. Incidents scoped to Nightly ledger load stay attached to Nightly ledger load, and a step's history stays continuous through refactoring.
This matters most for incidents caused by a change. The pipeline as it was before Tuesday's release and the pipeline after it are the same pipeline with different code, and that is exactly how the investigation needs to see them.
How an investigation uses it
Discovery is preparation. Its value shows up in the first phase of every investigation.
The first phase, Locate, ranks the discovered pipelines and steps against the incident
description and any linked ticket. For the sample report ("transaction volume dropped by 38% in
the nightly ledger load; finance says the EUR totals have been missing since Tuesday", with Jira
ticket FIN-2291) it matched Nightly ledger load at 91%, noted that it is the only pipeline
writing fact_transactions, and chose stg_transactions as the step to
start from. Each match cites the file and line range that produced it. If nothing matches well
enough, the investigation asks for more context instead of guessing.
The step graph then sets the order of the log sweep, most suspicious step first, and the code lines behind each step are what hypotheses cite. You can also narrow an incident yourself when opening it, to a repository, a pipeline or a single step.
What a repository cannot show
Discovery only knows what is in the repository. For many data teams, a meaningful part of the pipeline is not.
Pipeline definitions have moved into services. A job built in the AWS Glue console is stored by
Glue; there is no file. A Data Factory without git integration keeps its pipeline JSON in the
factory. SQL Server Agent jobs are rows in msdb. Stored procedures are routinely
altered in the database by someone fixing a problem at night, and the fix never reaches git. A
config-driven engine is the extreme case: the code is a generic interpreter, perfectly readable,
and the pipeline itself is rows in a control table.
None of these are visible to discovery today. The table below is the honest version.
| Definition lives in | Seen today? | What that means |
|---|---|---|
| Job spec, script or service code committed to the repository | Yes | Trigger, steps and code lines are all discovered |
Stored procedure kept as a .sql file in the repository | Yes | Traced as a step, as committed |
| Stored procedure altered directly in the database | No | Decim sees the committed version, which may no longer be what runs |
SQL Server Agent job defined only in msdb | No | The schedule and step list are invisible; code the steps call may still be found |
| AWS Glue visual job, or a script edited in the console | No | The definition is service state, not a file |
| Azure Data Factory pipeline without git integration | No | The pipeline JSON lives in the factory |
| Pipelines expressed as rows in a control table | Partly | The interpreter code is read; the rows that make it a pipeline are not |
| Parameters set at deployment or run time | Partly | Seen only where the value is written into the repository |
An investigation does not stop at the edge of the map. Logs show what actually ran, whether or not a step appears in the graph, and the log sweep reads them from Cloud Logging or through the Windows agent. When a step has no logs, the investigation says so rather than assuming it was fine. When the deciding fact is inside a database, Decim writes a read-only check for a person on your team to run and feeds the result back in. You can also attach exports, such as a scripted copy of an Agent job, as evidence for the incident. What Decim will not do is present a repository-only map as the whole pipeline.
Where this is going: provenance
This section describes a design direction. It is not shipped. Today every connection is marked only as direct or inferred, from code.
The blind spots above share a cause: one source of definitions. The planned scheduler, database and cloud ETL connectors would each add another, and the logs Decim already reads during investigations are evidence of what actually runs. With more than one source, a node in the graph could say not just where it was found but whether the sources agree.
The model we are designing towards gives every node and edge one of four provenance states.
| State | Meaning | What it would tell you |
|---|---|---|
| verified | In a definition and observed running | The part of the pipeline both sources agree on |
| declared | In a definition, never observed | A dead branch, or a step that quietly stopped |
| observed | Observed running, in no definition | Undocumented, and often where the rows went |
| drifted | Defined in more than one place, and they disagree | Only one of the copies executes |
The gap between declared and observed is the point. A step written down and never seen running is either a dead branch or something that quietly stopped. A step seen running that no definition describes is undocumented, and undocumented steps are where rows tend to go missing: the reject table added during an old incident, the staging table used once for a backfill. A drifted node, defined in two places that disagree, is a live defect in the release process, because only one copy executes.
The design rule behind it stays the same as today: no single source, code included, becomes the truth by default. Until those connectors ship, Decim marks what it found in code as direct or inferred, and says when the repository is all it has seen. The integrations page lists what is planned.
Common questions
Which languages and frameworks does discovery support?
.sql files and a YAML job spec are all read the same way. The trade-off is that a connection Decim had to infer is marked inferred, not presented as fact.Which source-control providers are supported?
Does discovery change anything in our repository?
Our stored procedures are not in git. Will Decim see them?
What if we have no repository connected at all?
Is this data lineage?
Related: investigations for how the step graph is used; evidence for how code lines and log lines are cited; remediations for the fix to the sample incident; and the product overview for how the parts fit together.
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.