Daml Triggers - Off-Ledger Automation in Daml
In a full-stack Daml application, there usually are two types of components that interact with the ledger in addition to the Daml templates that you upload to the ledger. The first type is a component that provides the link between the ledger and the outside world. This is often just a web UI but it can also be something that integrates with external services, e.g., a bridge to Slack. Second, there is often a component that interacts only with the ledger. This is usually some form of automation that waits for certain contracts to appear on the ledger and then creates new contracts or exercises choices based on that. Automation and UIs are an often important parts of the developer experience of full-stack DLT applications which are often overlooked when the focus is only on the smart contract language.
With Daml triggers, we aim to reduce some of the friction and repetition by allowing you to express automation that only interacts with the ledger directly in Daml. Thereby, you can reuse all of your types and functions that you have implemented as part of your Daml models.
To demonstrate how this looks in practice, we use a simple vacation tracker. Alice is a manager at Acme Corporation, which uses this Daml-based vacation tracker. Alice wants to leave it up to her employees to choose when they take their vacation. So instead of manually approving vacation requests, her goal is to implement a Daml trigger that will automatically approve all of them on her behalf. This is a simple example for the purpose of illustration, but you can imagine a different set of criteria for vacation requests that should be approved automatically.
You can find the full Daml model at https://github.com/cocreature/vacation-trigger/blob/master/src/Model.daml. In this blog, we focus on the two templates the trigger needs to interact with, namely Vacation and VacationRequest.
The implementation of AcceptRequest is not relevant for our trigger, so we have omitted it here.
To implement a Daml trigger, you need to provide 5 things:
- The type of your user-defined state. Most of the times this is simply the unit type ().
- A function to initialize the user-defined state on startup.
- A function to update the state when new messages arrive.
- A list of templates that the trigger is interested in.
- A rule method which sends commands to the ledger based on the current state of the Active Contract Set (ACS), the party the trigger is running as, the commands in flight (i.e., commands sent to the ledger which could still fail) and the user-defined state.
For this example, we do not need any user-defined state so we use () as our state type. Therefore the functions to initialise and update the user state become quite simple. We use AllInDar to indicate that we want to receive events for all templates in the DAR which is a safe but potentially less performant default. For this specific trigger, we could also use RegisteredTemplates [registeredTemplate @VacationRequest] at the cost of having to update this when our trigger starts using more templates.
In the rule of our auto-approve trigger, we first need to get all VacationRequest contracts. For that, there is a getContracts function that given the ACS returns a list of pairs of contract ids and the contract payload for a given template type. The auto-approve trigger will be running on behalf of a single party, so we have to filter the requests where this party is the boss (Alice in our example). Finally, we can call dedupExercise to exercise the AcceptRequest choice on each of those contracts. dedupExercise automatically makes sure that the choice is only executed if the same command is not already in flight, i.e., has already been sent by a previous run of this trigger. Putting this together the rule of our trigger is quite compact:
To run a trigger, you first compile it using daml build and then start it using daml trigger. The latter expects the path to the DAR, the name of the trigger in the form Module:identifier, the Ledger API host and port, and finally the party our trigger should run as.
daml trigger --dar=.daml/dist/vacation-trigger-0.0.1.dar --trigger-name=Trigger:autoApproveTrigger --ledger-host=localhost --ledger-port=6865 --ledger-party=Alice
This will start a long-running process for the trigger on your machine that connects to the ledger API. The trigger will run as Alice, so the boss argument in autoApproveRule will refer to Alice. Please take a look at the README (of this example project) for instructions on how to run the full example against an in-memory ledger and see the effects of the trigger in navigator.
To find more information on Daml triggers, go to our documentation.