Security change on December 1, 2026: TLS 1.2 will become the minimum standard for 365 business Print Agent. Details and system requirements
365 business development
CLI

Probing AL (albuild probe)

Compiling proves the syntax. Publishing and testing costs minutes, a container and a test app. Between the two sits a question AL has never had a way to ask: what does this actually return?

TerminalCode
albuild probe eval --vars 'r: Text' --code "r := CopyStr('ABC', 2, 10);" # r Text 'BC'

albuild probe runs one AL procedure, or a few AL statements, locally — in a fraction of a second, with no container, no publish and no test app.

A reasoning tool, not a gate

A probe says something about a function. It never says anything about a release. There is no service tier behind it, so a green probe is not a test and not a deployment check. Runtime validation stays albuild app deploy against a container.


How fast, really

Measured end to end, including .NET process start and reading the workspace:

Wall clock
No workspace (a self-contained expression)~130 ms
Against a real product workspace (~100 .al files)~350 ms

The evaluation itself is a few milliseconds; process start and parsing the workspace dominate. The comparison that matters is not against zero, it is against a container round-trip: publishing and running a test to answer the same question costs minutes.


How it works

An AL interpreter, shipped inside the CLI, reads the same AL source the compiler reads:

TierSource
1The workspace's own .al files
2Dependency source, pulled lazily out of the .app packages in .alpackages (a BC app package carries its full AL source under src/)
3Dependencies shipped without source — runtime packages — contribute their tables and enums from the compiled metadata

No Microsoft binaries are loaded and nothing is patched. Workspace source always wins over package source for the same object, so you probe what you are editing.


Commands

probe scope

Run this first in a repository. It lists every probeable object with its procedure signatures, the symbol packages found, and — importantly — every source file the engine could not read, so a gap never looks like an object that does not exist.

TerminalCode
albuild probe scope --project ./app albuild probe scope --json

probe call — one procedure with real arguments

TerminalCode
albuild probe call --target '"bdev.BNK Bank Formatter"::FormatIBAN' \ --args '["de89 3704 0044 0532 0130 00"]' --project ./app # -> Code[50] 'DE89 3704 0044 0532 0130 00'

--target is "<object>"::<procedure>; prefix it with Table or Codeunit when a name is ambiguous. Arguments are positional JSON, converted to each parameter's declared AL type — so a 60-character string into a Text[50] fails exactly as the AL would. var parameters are reported back under outputs[].

probe eval — an expression, with no object around it

TerminalCode
albuild probe eval --vars 'd: Date' --code "d := CalcDate('<CM>', 20260115D);" albuild probe eval --vars 'n: Integer' --code "n := StrPos('hello world', 'world');"

Declare variables in ordinary AL var syntax; every one is reported with its value and type.

probe batch — many probes, one process

The cost of a probe is the round trip, not the interpretation. Checking twenty inputs against one function should cost one invocation:

JSONCode
[ { "target": "\"bdev.BNK Bank Formatter\"::FormatIBAN", "args": ["de89370400440532013000"] }, { "target": "\"bdev.BNK Bank Formatter\"::MaskIBAN", "args": ["DE89370400440532013000"] }, { "code": "r := CopyStr('ABC', 2, 10);", "vars": "r: Text" } ]
TerminalCode
albuild probe batch probes.json --json

Exits non-zero if any probe in the batch failed.


Options

OptionWhat it does
--args '<json>'Positional arguments for probe call, converted to each parameter's declared type.
--set '<json>'Seed the in-memory tables: {"Currency":[{"Code":"EUR","ISO Code":"EUR"}]}.
--traceRecord every assignment and call with its value and source line.
--vars '<decls>'AL variable declarations for probe eval.
--project <dir>The AL project (or repo root) to load. Defaults to the working directory.
--today, --work-datePin Today() / WorkDate() so a probe is reproducible.
--user, --companyWhat UserId() / CompanyName() return.
--steps <n>Interpreter step budget before the run is abandoned (default 5,000,000).

Paths always resolve locally — the engine reads this machine's source, so --server plays no part.

Seeding data

There is no Business Central data. Every table starts empty; a Customer exists only if you put one there:

TerminalCode
albuild probe call --target '"bdev.BNK Bank Formatter"::GetISOCurrencyCodeOrDefault' \ --args '["EUR"]' --set '{"Currency":[{"Code":"EUR","ISO Code":"EUR"}]}'

Without the seed the same call raises Business Central's own error — "The General Ledger Setup does not exist…" — which is the honest answer, not a fabricated default.

Tracing

--trace prints each step with its value and source line, so an off-by-one is located in one invocation instead of five:

Code
trace: 67 call StrLen() 67 assign ibanLength := 12 72 call CopyStr() 72 assign visiblePart := '3000' 73 call PadStr() 73 assign maskedPart := '********'

Reading the result

Every result states the conditions it was produced under — pinned Today, UserId, the number of seeded rows, "no service tier, tables start empty". A value is only meaningful together with them.

Failures carry a stable code; branch on that rather than on the message text:

CodeMeaningExit
PROBE_AL_ERRORThe AL itself raised — Error(), TestField(), a length overflow. This is an answer: Business Central would do the same.1
PROBE_UNSUPPORTEDA real AL construct the engine does not implement. Not a defect in your code.1
PROBE_SYMBOL_ONLYThe implementation is not available as source (a runtime package).1
PROBE_UNKNOWN_TARGETNo such object, procedure, field or variable — the error lists what does exist.4
PROBE_SYNTAX / PROBE_BAD_ARGUMENTThe AL could not be parsed, or the arguments do not fit the signature.2
PROBE_LIMITThe step or time budget ran out — almost always a loop that never terminates.124
PROBE_WORKSPACENo AL project could be loaded.3

Every failure also carries the AL call stack (object, member, file, line).

A refusal is an answer, not an obstacle

The engine is built so it can only ever be incomplete, never wrong. Commit(), Random(), an unsupported CalcFormula and Format with a format string are refused rather than approximated, because a plausible-looking wrong value is worse than no value. Never rewrite AL until a probe stops refusing — probe a different path, or use a container.


What is not there

No service tier: no company, no permission system, no transactions, no UI, no HTTP, no background sessions. Tables start empty. Commit/Rollback are refused rather than silently treated as no-ops.

Base Application logic runs where its source is available. A dependency shipped as a runtime package contains no AL at all — its tables and enums are usable as data shapes, its code raises PROBE_SYMBOL_ONLY naming the package.

Also refused rather than approximated: array variables, with … do, .NET interop, Format format strings, CalcFields for anything but count/sum, and page or report rendering.


Fidelity

The engine is verified against a real Business Central container by the differential harness in the ALbuild repository (test/differential/). Both sides execute the same AL source, and the probe's answer becomes the expectation in a generated BC test, so any disagreement is reported with both values.

That process has found and fixed real defects — including two where the engine's own unit tests confidently asserted the wrong behaviour, because they were written from the same belief as the code. Facts it settled, worth knowing while writing AL:

  • text comparison is case-sensitive ('abc' = 'ABC' is false), and so is table filtering;
  • CopyStr truncates on overrun, but an overlong assignment to Text[n]/Code[n] raises;
  • Code is uppercased and trimmed on assignment, before the length check;
  • Format groups thousands for a Decimal (1,024) but not for an Integer (2026);
  • a discarded failing Get or FindSet raises;
  • a table's OnInsert trigger runs before the row is written.

Deliberately out of scope

albuild probe is CLI-only. It is not an Azure DevOps task, not an MCP tool and not part of any pipeline. Its value is in answering a question while you write code; a pipeline gate needs the real service tier, and that is what the DevOps tasks and albuild app deploy are for.

Last modified on