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. 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. 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. 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. 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. 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.

Triggers

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".

The trigger kinds Decim recognises, and what each tends to look like in code
Trigger In the code, for example Typical pipeline
ScheduleA cron expression in a job spec, a crontab line, a Hangfire RecurringJob.AddOrUpdate registrationNightly ledger load, 02:00 UTC
Queue consumerA handler bound to a queue, topic or subscriptionA service that loads each order event as it arrives
File arrivalA watcher on a directory, share or bucket prefixA daily partner file dropped into an inbound folder
CDCA handler on a change feed or change tableReplicating changed customer rows downstream
Batch commandAn entry point a person or another system runs: a CLI command, a console appA 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.

Example

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 — the trigger yaml
# 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.

Nightly ledger load, as discovered from acme/ledger-etl
Step Role Code behind it Connection in
Core banking ledgerSourceledger_etl/extract.py L18–27, the REST export URL and request—
Extract ledgerTransformationledger_etl/extract.py L31–74, writes raw.ledger_entriesDirect
stg_transactionsTransformation (stored procedure)sql/stg/usp_load_transactions.sql L1–48, called by the job stepDirect
FX rates feed (ECB daily rates)Sourceledger_etl/fx_rates.py L9–40, writes ref.fx_ratesInferred
Currency conversionTransformation (stored procedure)sql/fx/usp_convert_transactions.sql L1–51, joins ref.fx_ratesDirect
fact_transactionsDestination (warehouse table)sql/fx/usp_convert_transactions.sql L52–66, the INSERTDirect
Finance daily reportDestinationreports/finance_daily.sql L1–30, selects from fact_transactionsInferred

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 — the code behind the stg_transactions step sql
-- 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.

Connections

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.

Limits

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.

Where a definition lives, and whether a repository scan sees it today
Definition lives in Seen today? What that means
Job spec, script or service code committed to the repositoryYesTrigger, steps and code lines are all discovered
Stored procedure kept as a .sql file in the repositoryYesTraced as a step, as committed
Stored procedure altered directly in the databaseNoDecim sees the committed version, which may no longer be what runs
SQL Server Agent job defined only in msdbNoThe schedule and step list are invisible; code the steps call may still be found
AWS Glue visual job, or a script edited in the consoleNoThe definition is service state, not a file
Azure Data Factory pipeline without git integrationNoThe pipeline JSON lives in the factory
Pipelines expressed as rows in a control tablePartlyThe interpreter code is read; the rows that make it a pipeline are not
Parameters set at deployment or run timePartlySeen 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.

Direction

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.

Planned — not shipped: the four provenance states
State Meaning What it would tell you
verifiedIn a definition and observed runningThe part of the pipeline both sources agree on
declaredIn a definition, never observedA dead branch, or a step that quietly stopped
observedObserved running, in no definitionUndocumented, and often where the rows went
driftedDefined in more than one place, and they disagreeOnly 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?
It is not limited to a list. Decim reads code rather than running format-specific parsers, so a Python service, a .NET worker with Hangfire jobs, a Java batch job, a folder of .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?
GitHub, through the GitHub App, is the only provider today. GitLab, Azure Repos and Bitbucket are planned. See integrations for the current list.
Does discovery change anything in our repository?
No. Discovery reads. The only thing Decim ever writes to a repository is a draft pull request, and only when someone clicks to open one from a solution. See remediations.
Our stored procedures are not in git. Will Decim see them?
Not today. Discovery only sees procedures kept in the repository. A procedure that exists only in the database, or was altered there after its last commit, is invisible to discovery. Database connectors are planned. Until then, an investigation can still read that step's logs, and when the deciding fact is in the database Decim writes a read-only check for someone on your team to run.
What if we have no repository connected at all?
Discovery needs a repository, so there is no step graph. You can still open incidents and attach logs and exports, and the investigation works from that evidence and from any configured log sources. Locating the failing step is harder without code to match against, and the investigation says so when it needs more context.
Is this data lineage?
No. Lineage answers where a column came from, usually inside a warehouse that records it. Discovery answers which pipelines exist, what starts them, and which code runs at each step, across services and scripts that emit no lineage metadata. It is built to point an investigation at the right lines of code.

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.