Business workflows often involve more than one organization. A bank sends money to another bank. An insurer pays a hospital. A vendor licenses software to a customer.
These workflows need shared state, but no single party owns the truth. A regular database does not fit – someone has to host it, and the others must trust that host. Public blockchains are a better fit, but they make every transaction visible to everyone, which is highly unacceptable for regulated business data. This is where Canton Network comes in with its Daml language.
This post has two parts. A short section on the problem Daml solves, then a longer tour of the language: its syntax, how contracts and choices work, how to test them, and how to debug them locally.
Be sure to also read the article Canton Network Through a Distributed Systems Lens to learn more about the Canton Network itself.
What Daml is and why it exists
Daml is a language for writing smart contracts that run on a shared ledger between multiple parties. It was created by Digital Asset, a New York-based technology company founded in 2014 that builds distributed-ledger infrastructure for banks, insurers, exchanges, and other regulated institutions. The same company develops Canton Network – the ledger Daml is designed to run on – built for regulated, multi-party workflows.
Daml runs on a ledger (Canton Network) that enforces two rules:
- Only parties named on a contract can see it.
- Only parties named on a contract can change it.
Contracts are immutable. A state change archives the old contract and creates a new one. Every change has explicit authorization.
That is the whole model. The rest of this post is about how it looks in code.
Syntax basics
Daml is a functional language with syntax based on Haskell. This section covers the parts of the syntax needed to read the rest of this post.
Functions and currying
A function is declared with a type signature on one line and a definition on the next:
functionName– the function identifier. Must match exactly on both lines.ArgType1 ... ArgTypeN– the types of the arguments, in order, separated by->.ReturnType– the type of the value the function returns; always the last type in the chain.param1 ... paramN– the argument names bound in the body. The number of parameters must match the number of argument types.<body>– the expression evaluated when the function is called. There is noreturnkeyword; the value of the expression is the result.
For example, a function that adds tax to a price:
The signature reads as "takes a Decimal, takes another Decimal, returns a Decimal". Function application uses whitespace, not parentheses:
All functions in Daml are curried by default. addTax above is really a function that takes one Decimal and returns a function from Decimal to Decimal. Partial application follows for free:
In Scala, currying and partial application are opt-in through methods like .curried on function values, or by writing multiple parameter lists. In Daml, they are the default; every multi-argument function can be partially applied.
Lambdas use a backslash for the leading argument list:
Values and bindings
There are no mutable variables in Daml. Every binding is fixed for the rest of its scope. Local bindings use let ... in ... inside expressions:
Inside a do block, let works the same way – one binding per line, no in needed. Effectful actions bind their result with <-:
The difference between let x = ... and x <- ... inside a do block is important. let binds a pure value – here greeting, computed from alice. <- extracts the result of an effect – here alice, the Party returned by allocateParty. Both bindings are usable in the following lines: alice is used to build greeting, and greeting is passed to debug.
The do block itself lives inside an effectful context. In this example, it is a Script (Daml's testing runtime, covered later in the post). The same shape appears inside Update (the effect type for ledger actions) and inside the body of a choice on a template – both introduced in the sections that follow. For now, the point is only the syntax: let for pure bindings, <- for the result of an effect.
The same shape in Scala is a for-comprehension over some effect type (IO, ZIO, Future, or a custom Script[_] type):
The mapping between the two is direct: let x = expr in Daml corresponds to x = expr in a Scala for-comprehension (no arrow, pure binding), and x <- effect is identical in both languages (bind the result of an effect).
Records
A record has named fields declared with with:
Values are constructed field-by-field:
Records are immutable. To "update" a field, a new record is built from an existing one using the same with syntax:
Field access uses dot notation: usd10.currency returns "USD".
Sum types and pattern matching
Sum types combine several constructors under one name:
Pattern matching with case inspects the constructor and pulls out its fields:
The final _ is a wildcard – it matches anything not covered by the branches above and binds no variable. It is useful when several constructors should be handled the same way, or as a defensive catch-all. Without a wildcard, the compiler warns when a case expression does not cover every constructor, so missing branches show up at compile time rather than at run time.
Type classes, not inheritance
Daml has no inheritance, no subclassing, no protected fields, and no this-based method calls. Data types have no methods; behavior is defined by separate functions.
Ad-hoc polymorphism is expressed with type classes, the same mechanism Haskell uses. The deriving (Eq, Show) clause in the records above is how the compiler automatically instantiates the built-in Eq and Show classes. A custom type class is declared with a class block that lists the required operations, and one instance per type that supports it:
- ClassName – the name of the type class.
- a – a type variable that stands for any type implementing the class. It appears in the operations' signatures.
- operation1, operation2 – the operations every instance must provide. Each has a type signature written in terms of
a. - instance ClassName SomeType – declares that
SomeTypeimplements the class by providing definitions for each operation.
For example, a Describable class with a single describe operation, plus instances for Money and TransferStatus:
Any function that takes a Describable a argument works for every type that has an instance:
The same function in Scala 3, with Describable as a trait and instances provided by given definitions:
The Daml constraint Describable a => becomes the Scala parameter (using d: Describable[A]); the call describe x becomes d.describe(x). Both compilers do the same thing under the hood – they look up the correct instance based on the argument's type and pass it into the function.
What is missing compared to an OOP language is worth noting: no extends, no method overriding, no shared mutable state on a type. Templates (introduced in the next section) look object-like at first glance, but they are still just records – the choices inside them are not methods; they are separately-typed actions bound to the template.
No null, no exceptions
There is no null. Optional values use Optional a, with constructors Some a and None:
There is no exception mechanism for business errors. When something is wrong, assertMsg aborts the current transaction with a message:
Because the entire transaction is atomic (see the next section), a failed assertion leaves the ledger untouched – no half-applied state, no cleanup code required.
Templates: the core building block
A template describes a contract that can live on the ledger. A template has three parts: fields (the data), rules about which parties are involved, and choices (the actions that can be taken).
A simple bank account:
Four things to notice:
- signatory bank – the bank is the party that agreed to this contract. Without the bank's authorization, the contract cannot be created. A contract can have multiple signatories, in which case all of them must agree.
- observer owner – the owner can view the contract but cannot sign it. Observers are read-only participants. Anyone who is neither a signatory nor an observer cannot see the contract at all.
- controller – each choice names the party allowed to trigger it.
Depositcan only be triggered by the bank;Withdrawcan only be triggered by the owner. This is not a runtime check that the choice body has to remember to make. It is part of the contract's definition, and the ledger refuses to run the choice at all if the caller is not the controller. - create this with balance = ... – contracts are immutable. A choice that changes state archives the current contract and creates a new one with the new field values.
thisrefers to the current contract;with balance = ...is record-update syntax.
signatory, observer, controller, choice (and ensure, key, maintainer, which appear in other templates) are contextual keywords – the Daml parser recognizes them only inside a template ... where block. Each one introduces a specific slot in the template definition, and the expression that follows fills that slot. Outside a template, they are ordinary identifiers with no special meaning.
How a choice actually executes
The details of what happens when a choice is exercised are worth explaining, because they explain a lot about how the ledger works.
A party submits a command to the ledger: exercise the choice Withdraw on account cid_alice with amount 30. From there, the following steps happen:

- Authorization check. The ledger looks at the target contract and the choice being exercised, finds the controller (in this case,
owner), and checks that the submitting party matches. If it does not, the command is rejected immediately. The choice body never runs. - Interpretation. The choice body runs in an interpreter. Every effect it performs –
create,archive,fetch,exerciseon another contract – is recorded as a node in a tree structure. This tree is called the transaction. Nothing is committed to the ledger yet. - Consumption. By default, choices are consuming. Exercising a consuming choice archives the contract it was called on, as part of the same transaction. In the
Withdrawexample, theAccounton whichWithdrawwas exercised is archived, and a newAccountwith the updated balance is created. Both events are part of the same transaction, so from the outside they appear to occur at the same instant. - Validation. Once the interpreter finishes, the ledger validates the whole transaction: every contract fetched or archived must remain active; every signatory of every newly created contract must have authorized the transaction; all assertions inside the choice bodies must have passed.
- Atomic commit. If validation passes, the entire transaction is committed at once. All archives and creates apply together. If any part failed – an assertion, an authorization check, a stale contract reference – nothing applies. There is no partial state change.
Why there is no double-spending
The archive step is what prevents double-spending. Consider two Withdraw commands submitted at the same time, both against Alice's account, both for the full balance.
Both commands go through the interpreter independently. The ledger then orders them (Canton uses a consensus mechanism to agree on an order across participants). The first one commits: Alice's original account contract is now marked archived, and a new account with the reduced balance takes its place. When the second command reaches validation, the account contract it was built against no longer exists on the ledger. Validation fails, and the second transaction is rejected.
This is different from optimistic locking in a database, where the developer has to manage version fields and check them by hand. In Daml, every contract has an implicit active/archived status, and every operation that touches a contract is automatically validated against that status.
A two-party workflow: proposal + accept
A single-party contract is not very interesting. Real workflows involve two or more parties who must agree. The standard pattern is proposal + accept:
fromOwner creates the proposal by signing it alone. toOwner sees it (as an observer) and can either Accept or Reject. Neither party can change the other's account by themselves; both actions require the other's involvement – either as a signatory on the account, or as the controller who triggered the choice.
Inside Accept, several things happen inside one transaction: two accounts are fetched, two are archived, and two new ones are created. Because it is a single transaction, the entire transfer is atomic. It is not possible for the sender's balance to go down without the receiver's going up.
The do block is Haskell's do-notation. In Scala with an effect system such as cats-effect or ZIO, the same shape is a for-comprehension:
Sequential composition of effects, one line per step, <- binds the result.
Parties and ContractIds
Two Daml types are worth calling out:
- Party – the identity of a participant. Opaque, managed by the ledger. Not a public key or an email address. In Canton, party allocation and signing are handled by the participant node.
- ContractId t – a typed reference to a contract of type
ton the ledger. Similar to a typed referenceRef[t]in Scala, but the reference points to something the ledger stores and validates.
Three basic operations work with these types:
create : t -> Update (ContractId t)– put a new contract on the ledger.fetch : ContractId t -> Update t– load an existing contract.archive : ContractId t -> Update ()– remove an existing contract.
These are the ledger equivalents of insert, get, and delete.
Testing with Daml Script
Daml has a built-in scripting language (written in Daml itself) for testing workflows. A script allocates parties, creates contracts, exercises choices, and checks results. Scripts run against a local sandbox ledger that behaves like the real Canton ledger for authorization and validation.
A test for the transfer workflow:
The shape is close to a standard test fixture: allocate actors, set up state, run the workflow, and assert the outcome. submit bank do ... runs commands as the given party. If the party is not authorized to run those commands, the script fails with an authorization error – the same error that would come back from the ledger in production.
Because the sandbox runs the same authorization and validation logic as Canton, tests exercise the real rules. If a script tries to make Alice do something only Bob can do, it fails the same way it would in production. The tests are not simulating the ledger – they are running it.
Debugging: transaction trees
Every successful command on the ledger produces a transaction. A transaction is not flat; it is a tree of actions. When a choice is exercised, all the effects it performs (create, archive, fetch, and any nested exercises on other contracts) become child nodes of that exercise. Nested choices produce nested subtrees, all the way down.
When a script runs, the sandbox can print the transaction tree for each command. For the Accept step in the test above, the tree looks roughly like this:
Each node records what happened, which contract was involved, and who authorized it. The tree makes it easy to see:
- Every state change caused by a single command, in order.
- Which contracts were archived, and which were newly created.
- Which parties authorized which parts of the transaction.
- Whether an assertion or authorization check caused the transaction to fail, and at which node.
The transaction tree can be printed with debug calls within a choice, with submit in verbose mode within a script, or via the Daml Studio UI (see below). When a workflow does not behave as expected, the transaction tree is the first place to look. It shows the actual sequence of events on the ledger – not a stack trace, but the ledger's record of what happened.
Two other debugging tools help:
debug : Text -> Update ()– prints a message to the script's log during execution. Useful for inspecting intermediate values inside a choice body.trace : Text -> a -> a– prints a message and returns its second argument unchanged. Useful for inspecting values inside pure expressions without changing the code structure.
Tooling: Daml Studio and IntelliJ
Digital Asset ships a Visual Studio Code extension called Daml Studio. Installing the Daml SDK also installs this extension. Its main features:
- Syntax highlighting and code formatting.
- Type checking directly in the editor, with inline error messages. The Daml compiler runs incrementally as code is edited.
- Go-to-definition, symbol lookup, and hover types.
- Script runner with inline results. Any
Script ()value in the file is picked up automatically. Clicking "Script results" opens a panel that runs the script against the sandbox and shows the resulting ledger state, the parties involved, and the full transaction tree – all rendered as an interactive view. This is the fastest way to iterate on a workflow: write a template, write a small script that exercises it, and watch the results update as you type. - A scenario view of active contracts from any party's perspective, which is useful for verifying that visibility rules match expectations.
For developers who prefer JetBrains tools, there is a community IntelliJ IDEA plugin, daml-canton-jetbrains, maintained by Moonsong Labs. It provides syntax highlighting, code navigation, and integration with the Daml compiler for IntelliJ-based IDEs. The feature set is not identical to VS Code – Daml Studio is still the reference environment – but for people who already work in IntelliJ, the plugin covers the essentials.
Trying it out
To install the SDK and start a local project:
daml start compiles the project, runs a sandbox ledger, and launches a web UI (Navigator) where contracts can be created and choices exercised from each party's perspective. Combined with Daml Studio, this is enough to build, test, and inspect a workflow end-to-end without any external services.
Summary
Daml is a functional language for writing contracts that run on a shared ledger. The main ideas are:
- Templates define contracts: their fields, the parties involved, and the actions (choices) that can be taken on them.
- Signatories, observers, and controllers are declarative authorization rules that the ledger enforces.
- Contracts are immutable. State changes work by archiving the current contract and creating a new one, all inside a single atomic transaction.
- Every command produces a transaction tree, which is the primary tool for understanding and debugging what actually happened.
- Daml Script provides local testing against a sandbox that runs the same rules as production Canton.
Article reviewed by Adam Rybicki
Looking for a Daml smart contract development team?
If your organization is planning to build on Canton Network or deploy enterprise-grade Daml applications, contact VirtusLab. We have hands-on experience with distributed ledger infrastructure for regulated industries.

