import { Callout } from "zudoku/ui/Callout";

# Call graph

ALbuild builds the **call graph** of your AL apps on every build: which method calls which, across your apps and into Microsoft's. From it, every build reports what it contains, what no test reaches, what is dead or calls obsolete code, and **what a change affects**, compared with the previous build. A short architecture file turns the graph into a **map of the app**: its parts, the flows it exists for and a tour for new developers. Agents query the same map instead of searching the code.

<Callout type="info" title="Built on the AL Tool">
The graph comes from Microsoft's `al graph` command, part of the AL Tool from **version 18.0.39 (Business Central 29)**. ALbuild picks the AL Tool exactly as for compiling, from the BC version you build against. For BC 28 and older the AL Tool has no `al graph`, so the step is skipped with a warning.
</Callout>

The PowerShell module does the work; the [Azure DevOps tasks](../devops-extension/build-tasks#analyze-call-graph), the [CLI](../cli/commands#call-graph) and the [MCP server](../mcp-server/index) are thin surfaces over it.

---

## How the graph is built

All AL projects of the workspace are compiled **together** with the source of their dependencies (`al graph extract-whole`). That matters: compiled one by one, the 365 business Banking test app lost all 6,342 of its calls into the app.

- **Microsoft's apps** (Base Application, System Application, Business Foundation, ...) come from the BC artifact. Their source is unpacked once per app and version and cached on the agent.
- **Other dependencies** are compiled along when their package ships source. A dependency **without source** is listed in the report: calls into it are missing, and so are calls that merely pass one of its types, because the compiler cannot bind them.
- The whole-corpus graph is large (Banking: 141,340 nodes, 573,151 calls), almost all of it Microsoft-internal. It is pruned to **your apps and their boundary**: everything in your apps, plus the Microsoft methods and events they touch (Banking: 13,911 nodes, 78,541 calls).

On a warm agent this costs about a minute for an app the size of Banking; the first build of a BC version also unpacks Microsoft's source once.

## What every build reports

| Figure | Meaning |
| --- | --- |
| Objects, methods, triggers, calls | The size of your apps. Calls are split by kind: direct, event, interface, trigger. |
| Calls into dependencies | How much your code relies on Microsoft's and other apps. |
| Event publishers / subscribers, interface implementations | The extension points your apps offer and use. |
| Test methods | `[Test]` procedures found in the source. |
| Not reached by tests | Product methods and triggers that no `[Test]` method reaches along the call graph. |
| Obsolete calls | Product code calling a method or object marked obsolete. Test code is not counted. |
| Dead methods | Non-public methods nothing calls. |
| Unassigned / described objects | Own objects no part claims, and objects with a description. See [the map of the app](#the-map-of-the-app). |

With a baseline every figure also shows its change against the previous build.

<Callout type="note" title="Static, and honest about it">
"Not reached by tests" is static reachability, not measured coverage: events the **platform** raises (table triggers such as `OnAfterModifyEvent`) are not followed. Use it next to [code coverage](code-coverage), which measures what actually ran.
</Callout>

### Dead code without false alarms

A call graph alone would report far too much dead code. ALbuild counts a method as dead only when nothing reaches it in any of these ways:

- **A call in the graph.**
- **An event subscription:** `[EventSubscriber]` methods are entry points.
- **Interface dispatch.** `al graph` resolves interface calls to one overload only, so every overload of an interface method counts. A codeunit that implements an interface from an app without source keeps all its non-local methods.
- **A call by name:** a string literal with the method name, e.g. a notification action.
- **A call the compiler could not bind:** the name is called somewhere in the source text.

On Banking these rules took the count from 301 to 36 dead methods, each one checked against the source.

### AL checks

The report also lists, **for information only**:

- own objects with inherent permissions or entitlements,
- Microsoft objects with inherent permissions or entitlements that your code calls,
- `[NonDebuggable]` product methods.

These lists never fail a build and have no gate.

<Callout type="note" title="Why cloud-to-OnPrem is not checked">
A check for cloud code that reaches OnPrem-only members was measured and left out. On Banking, OnPrem members are reached only through Microsoft's own calls (53 nodes, none called directly), so the check would raise nothing but false alarms.
</Callout>

## Impact: what a change affects

Every build compares its graph with a **baseline**: a pull request build with the last successful build of its target branch, any other build with the last successful build of its own branch. The baseline is the `callgraph` artifact of that build.

| List | Meaning |
| --- | --- |
| Added / removed | Methods and triggers that appeared or disappeared. |
| Modified | Members whose calls changed, or whose body text changed (per-member source hashes). |
| Affected callers | Everything that transitively calls a changed member, or called a removed one. |
| Tests reaching the change | The `[Test]` methods that reach a changed member: the tests to run first. |

## The map of the app

A graph of thousands of nodes does not explain an app. The map groups your objects into **parts**, describes them and shows how they connect.

The parts come from `callgraph.architecture.json` in the **repository root**. A person or an agent writes it locally and commits it. **The pipeline never calls AI**: it only reads the file.

- **Without the file**, the parts come from the folders: one part per folder of each app's source (below an optional `src/`), split one level further when a folder holds many objects. Test apps get one part each. Every repository gets a readable map this way.
- **Descriptions:** the `///` summary above an object's declaration always describes it. The file only adds descriptions for objects without one, or overrides them.
- **A broken file** does not break the report: the map falls back to the folders and says why.

### The file

An excerpt from 365 business Banking, with shortened texts:

```json
{
  "$schema": "albuild/callgraph-architecture@1",
  "project": {
    "name": "365 business Banking",
    "summary": "Connects bank accounts to banks and payment service providers."
  },
  "parts": [
    {
      "id": "bank",
      "label": "Bank",
      "kind": "feature",
      "summary": "The company's own bank accounts: accounts, payments, direct debits, reconciliation.",
      "paths": ["app/src/Bank/**"]
    },
    {
      "id": "bank.reconciliation",
      "label": "Reconciliation",
      "kind": "feature",
      "parent": "bank",
      "summary": "Turns retrieved bank transactions into payment reconciliation journal lines and applies them.",
      "paths": ["app/src/Bank/Reconciliation/**"]
    }
  ],
  "objects": {
    "Table bdev.BNK Banking User": "A user known to a banking provider. Usually just the 'DEFAULT' record for the whole company."
  },
  "flows": [
    {
      "id": "connect-bank-account",
      "title": "Connect a bank account (finAPI)",
      "steps": [
        { "node": "PageExtension bdev.BNK Bank Account List", "title": "Connect action", "note": "The user starts Connect bank accounts." },
        { "node": "Codeunit bdev.BNK Bank Account", "title": "Open the wizard", "note": "ConnectBankAccounts runs the connect wizard." },
        { "node": "Page bdev.BNK Connect Provider", "title": "Generic wizard", "note": "The user picks a provider." }
      ]
    }
  ],
  "tour": [
    { "node": "TableExtension bdev.BNK Bank Account", "title": "Start at the bank account", "note": "A bank account names two services: AIS() and PIS()." },
    { "node": "banking.model", "title": "The model tables", "note": "Provider-neutral temporary tables the contracts exchange." }
  ]
}
```

| Key | Meaning |
| --- | --- |
| `project` | Name and summary of the app. |
| `parts` | `id`, `label`, `summary`; `parent` nests a part in another. `kind` is one of `app`, `feature`, `service`, `integration`, `data`, `ui`, `setup`, `library`, `test`, `external` (default `feature`). |
| `parts[].paths` | Globs on the source path: `**` any folders, `*` within one segment, `?` one character, `{a,b}` alternatives. A pattern without a wildcard also matches as a folder. |
| `parts[].objects` | Patterns on `Type Name` or `Type Id`, same globs, e.g. `Codeunit bdev.BNK API*`. |
| `objects` | Descriptions by `Type Name`, for objects without a `///` summary or to override it. |
| `flows` | What the app exists for, step by step. Each step names a `node` (an object or a part id), a `title` and a `note`. |
| `tour` | The stops shown on the report's first open, in the same form as flow steps. |

An object belongs to the **deepest** part that claims it. Between parts of the same depth, an `objects` pattern beats a `paths` pattern, and the more specific pattern wins.

<Callout type="info" title="Banking in figures">
The Banking file has 51 parts, 5 flows and an 8-stop tour. All 935 objects are assigned; 628 are described, 130 of them in the file and the rest by `///`.
</Callout>

### Write and check it

```bash
albuild graph describe --worksheets .albuild/callgraph/worksheets
albuild graph describe --check
```

`graph describe` writes a starting file **without AI**: the folder-based parts, empty flows and an empty tour. With `--worksheets` it also writes one Markdown worksheet per part: its objects, their members and what they use. Write the summaries, flows and tour from them, by hand or with an agent.

`graph describe --check` validates the file against the graph and exits `1` on problems (`--no-fail` reports only):

- unknown parts and nodes, with a suggestion,
- patterns that match nothing,
- flow steps that do not call each other.

In PowerShell: `New-BcCallGraphArchitecture` and `Test-BcCallGraphArchitecture`.

## In the pipeline

```yaml
- task: GetBcArtifact@0          # sets $(bcArtifactUrl)
- task: CompileApp@0
- task: AnalyzeCallGraph@0
  inputs:
    artifactUrl: '$(bcArtifactUrl)'
```

`AnalyzeCallGraph@0` is `BuildCallGraph@0` followed by `ReportCallGraph@0`. Both pick up `callgraph.architecture.json` from the workspace root; there is no input for it. Use the two separately to build and report in different steps or jobs. Every build then gets:

- a **Call graph** tab: the interactive report. It follows the Azure DevOps theme, light or dark.
  - **Map:** the parts as cards with their calls between them. Open a part down to its objects and methods, with breadcrumbs and search; select a card for its summary, what it calls and what calls it. A **lens** decides the only colour on the map: test reach, structure (the kind of part), changes since the baseline, or dead code. Solid lines are calls out of the selection, dashed lines calls into it. The arrow keys follow the calls.
  - **Pages:** everything a page, report, query or xmlport reaches. Test doubles are left out.
  - **Flows:** each flow step by step. The **guide** (the tour) opens on first use.
  - **Impact**, **Findings** (not reached by tests, dead code and obsolete calls in one tab), **AL checks**, and **Apps** with any architecture problems.
- the figures, the impact and the top findings in the **build summary**.
- the `callgraph` **artifact**: the graph, the analysis and the report, and the next build's baseline.

Building or reporting the call graph **never fails a build**. Only a gate you set does:

| Input | Fails the build when |
| --- | --- |
| `maxNotReachedPercent` | more than this percentage of product methods is not reached by tests |
| `maxDeadMethods` | more dead methods are found |
| `failOnObsoleteCalls` | product code calls obsolete code |

The central `ci.yml` template runs `AnalyzeCallGraph@0` after the compile (parameter `callGraph`, on by default) and sets no gate.

## Locally

```bash
albuild graph build --workspace . --country de --bc-version 29.0
albuild graph report --open
albuild graph query --to "obj:My Codeunit" --depth 1
albuild graph impact --baseline ../main/.albuild/callgraph
```

The graph is written to `.albuild/callgraph` in the workspace. `graph report --open` opens the same interactive report the build tab shows.

## Lookups for agents

An agent that needs to know where something is can ask the graph instead of searching the code. The lookups are read-only and need neither the AL Tool nor a container.

```bash
albuild graph overview
albuild graph find ebics order
albuild graph show Codeunit bdev.BNK API Client
albuild graph path --from "Page bdev.BNK Bank Access" --to "Codeunit bdev.BNK API Client"
```

| Lookup | Answers |
| --- | --- |
| `overview` | The parts, what each is for, and the strongest connections between them. |
| `find` | Parts, objects and methods whose name or description contains all the words. |
| `show` | One part, object or method: description, part, members, what it uses and what uses it. |
| `path` | The shortest call chain between two objects or parts. On Banking the example returns Bank Access → Banking User → API Client. |

The [MCP server](../mcp-server/index) offers the same as `graph-overview`, `graph-find`, `graph-show` and `graph-path`, plus `graph-describe`, `graph-check` and `graph-hint`. In PowerShell it is `Get-BcCallGraphInfo`.

`albuild graph hint` (`Set-BcCallGraphAgentHint`) writes a marked section into `CLAUDE.md` that points agents at these lookups. The rest of the file is left alone. `--file` picks another file such as `AGENTS.md`; `--remove` takes the section out again.
