Skip to content

Modeling a simple use-case in NPL

Learn how to model and implement a document approval process using NPL core constructs, state machines, and multi-party workflows.

Prerequisites

Learning outcomes

After this track, you'll be able to:

  • Understand the NPL paradigm: Differentiate NPL from traditional programming languages and recognize its strengths in modeling multi-party workflows
  • Master NPL's core constructs: Learn key constructs such as protocols, permissions, and parties, and their role in enforcing business rules
  • Adopt the NPL mindset: Approach development with a contract-driven, state-oriented perspective aligned with NPL's declarative nature
  • Build real-world models: Translate complex processes into scalable and transparent NPL workflows
  • Use NPL tooling: Efficiently leverage IDE features, documentation, and testing frameworks to accelerate development

This walkthrough describes the key concepts with a bank account modelling. It lets you explore NPL by implementing a document approval process in the hands-on parts.

Problem introduction: building a document approval process

The document approval process

In this guide, you will model and implement a document approval process using the Noumena Protocol Language (NPL).

Initially, a document editor creates a document and edits it. Then, the document editor requests an approval. The approver reviews the document and approves it. This creates a 4-eyes check for the document involving two different users.

The process includes the following steps:

CreatedDocument createdInReviewDocument in reviewApprovedDocument approvedEdit documentRequest approvalApprove document

Walkthrough

This walkthrough will guide you step-by-step through the process of modeling a document approval process.

If you need help at any point, check out the NPL documentation. Syntax helpers along the exercises point to relevant sections of the documentation. A convenient NPL cheat sheet is also available with the most important information.

Repository setup

Theory

A simple but important thing to always keep in mind when setting up an NPL project is to set the source directories. This will allow the NPL Plugin in your IDE to recognise the source files.

Practice

Make sure you have completed the following tracks:

In the Creating a new NPL project track, you created a new NPL project. Open this project in your IDE.

You should now be able to run a command to verify that the project is set up correctly.

npl test --sourceDir api/src/main/npl

This should return no errors.

Packages

Theory

In NPL, packages are used to create a separation of concerns. Each package relates to a specific domain of the business problem, set of processes or data containers conceptually related.

The business modelling within a package is done using protocols. They represent objects, processes or data containers. To model a business problem, you will need to create one or more protocols. It is good practice to isolate each protocol in its own file.

Each protocol file is associated to a package via an explicit declaration within the file itself: package packageName, in addition to their location within the file system.

Reference:

Practice

In the main/npl folder, create a new folder named document-approval to represent the package for the document approval process.

Within the document-approval package, create a new file called document.npl

In document.npl, add the package declaration: package document-approval

Protocols

The first step in the implementation of the document approval process is to model the template for the document process. In NPL, this is done using a protocol. For this example, we have a simple process that allows a document editor to edit the document content and a document approver to approve the document.

The relatively simple document approval process can be modelled using a single protocol. For more complex processes, it is good practice to create multiple protocols, using protocol references to compose them. To split a process into multiple protocols, it is good practice to use a sequence diagram or a use case diagram to isolate the different concerns, a sub-process, or different actors.

Theory

Protocols are the core constructs of NPL, they are a template for an interaction (around a process, around a set of data, etc.). Protocols define such interactions in terms of permissions and obligations, and bind together the different parties and their actions. Protocol definitions do not pertain to specific parties, but rather define parties in terms of roles such as 'editor' and 'approver' in a very specific context.

Conceptually, protocols are somewhat similar to classes from object-oriented programming. When modeling business logic in NPL, protocols are particularly useful to represent templates that:

  • will be created multiple times,
  • have an own complex lifecycle, and
  • can be accessed by multiple parties, each with different permissions.

The header of a protocol definition starts with the keyword protocol, and is immediately followed by the protocol parties. The protocol name and its constructor parameters follow immediately after.

Here is an example of a protocol definition for a bank account:

package bank;

@api
protocol[account_holder, bank] Account(
    var accountHolderName: Text
) {
    // Protocol body
}

As you are editing, the NPL-Dev plugin gives you syntax highlighting and underlines syntax errors (e.g. malformed protocol header, missing semicolon).

Reference:

Practice

We will now create a Document protocol to streamline the interaction between the document editor and the document approver. It will enable the document editor to specify and edit the document content, and the document approver to approve the document. We will also get some help from the NPL-Dev plugin and verify that the code compiles correctly.

The parties defined here are the roles that users can play in the document approval process. It is not yet defined which user(s) will play which role.

Note

Parties are a type of variable in NPL. They are declared in the protocol header, and can be used in the protocol body. The party variables are assigned to the parties when the protocol is instantiated, from other party variables or a set of claims.

For more information on parties, see Parties.

In the newly-created file document.npl, copy-paste the structure of the Account protocol:

package bank;

@api
protocol[account_holder, bank] Account(
    var accountHolderName: Text
) {
    // Protocol body
}

Then adjust it to match the document approval process:

  • Define the two parties that will have access to the protocol - editor and approver. Parties here should be comma-separated
  • Name the protocol Document
  • Define an attribute for the constructor: content, of type Text. The attribute should be explicitly declared as a variable (var), so that it is stored and can be re-used as a protocol field after instantiation
  • Remove the comma between editor and approver. Note that the NPL parser will highlight an error
  • Re-insert the comma separating the two parties to come back to a valid protocol declaration
Solution
protocol[editor, approver] Document(
    var content: Text
) {

}

Permissions

Following the declaration of the Document protocol as the template for the document approval process, we need to define the actions that the different parties can perform on the document. Each user action can be undertaken by specific people only, and for some actions, only at specific times. Using a sequence diagram or a use case diagram, we can identify the different actions and the different parties involved. For each user action, we need to define a permission.

Theory

Permissions represent actions users can perform in relation to a protocol. They are declared within the protocol body. The declaration specifies what parties can invoke the permission, the input parameters, whether the permission should return any value, and potential restrictions on invoking the permission (which will be discussed later on).

In our bank account example, we can create a permission to allow the bank to load funds into an account. This permission can be invoked by the user playing the bank role only.

package bank;

@api
protocol[account_holder, bank] Account(
    var accountHolderName: Text
) {

    var balance: Number = 0;

    permission[bank] loadFunds(amount: Number) {
        // Permission body
    }
}

Reference:

Practice

Within the Document protocol, we are going to create an edit permission, which takes as parameter the new content. The goal of this permission is to allow the document editor to edit the document content.

Within Document, copy-paste the permission declaration from the bank account example above, and adjust it to match the document approval process:

  • Specify editor as the party that will have access to the permission
  • Name the permission edit
  • Define the input parameter: newContent, of type Text
  • Assign the new content to the content field
Solution
protocol[editor, approver] Document(
    var content: Text
) {
    permission[editor] edit(newContent: Text) {
        content = newContent;
    }
}

States

Along the designed process, some actions are only allowed at specific times. For example, the document editor can edit the document content at the beginning of the process, and not anymore after requesting an approval. This is where states come in. Try identifying the differences in behaviour along the process, or for your data objects. If there is clear progress in the sequence of actions that are needed, this can be modelled using states.

Theory

NPL allows to track the progress of processes through protocol state machines. State machines are composed of states, one being active at any given time. One of the states must be an initial state. It is also possible to define final states, meaning that once they are reached the state of the protocol cannot be changed anymore. Multiple states may be final.

package bank;

@api
protocol[account_holder, bank] Account(
    var accountHolderName: Text
) {

    initial state active
    final state frozen

    // Protocol body
}

To illustrate the progress within a process, a state transition is declared with the keyword become followed by the new state. It is also possible to define state guards for protocol actions such as permissions. This is done using the pipe symbol | following the optional return type, and followed by a comma-separated list of protocol states. This state guard ensures that the permission or obligation can only be called in the listed state(s). If no state guard is specified, the permission or obligation is allowed in all states.

package bank;

@api
protocol[account_holder, bank] Account(
    var accountHolderName: Text
) {

    initial state active
    final state frozen

    var balance: Number = 0;

    permission[bank] loadFunds(amount: Number) | active {
        balance = balance + amount;

        if (balance >= 10000) {
            become frozen;
        }
    }
}

Reference:

Practice

We can track the state of the document approval process. The document can take three states: an initial state created, a state inReview and a final state approved. We can use state guards to define when an action is allowed.

Within the Document protocol, add three states at the beginning of the protocol body:

  • Add three states at the beginning of the protocol body:

    • an initial state created
    • a state inReview
    • a final state approved
  • Update the edit permission to use state guard and ensure the protocol is in the created state when the permission is called.

  • Create a new permission requestReview that allows the editor to request an approval. This permission should transition the protocol to the inReview state.
  • Create a new permission approve that allows the approver to approve the document once it is in state inReview. This permission should transition the protocol to the approved state.
Solution
protocol[editor, approver] Document(
    var content: Text
) {
    initial state created
    state inReview
    final state approved

    permission[editor] edit(newContent: Text) | created {
        content = newContent;
    }

    permission[editor] requestReview() {
        become inReview
    }

    permission[approver] approve() | inReview {
        become approved
    }
}

Tests

We now have an initial behaviour for the document approval process. We can test it to ensure that it works as expected, and avoid breaking the process later on.

For more advanced developers, implementing tests comes before developing the business logic.

Theory

NPL offers a default testing framework to run unit tests. The framework is based on test functions. It is recommended for the test file to be in the same package as the file being tested (as specified by the package statements in the respective source files).

Each test function is marked via a @test decorator. The function takes an object of type Test as input, which will be used to call Test methods throughout the execution of the test.

@test
function testEqPassing(test: Test) -> {
    test.assertEquals(someValue, theSameValue, "informative message");
}

Here’s an overview of some of the most important Test methods.

  • assertTrue: Asserts that the provided value is true. Optionally a message may be provided that is displayed instead of the default message.

    test.assertTrue(conditionThatIsTrue, "The pump should have been turned on.");
  • assertEquals: Asserts that the provided value is equal to the expected value. Optionally a message may be provided that is displayed instead of the default message.

    test.assertEquals(someValue, theSameValue, "Account was not successfully updated");
  • comment: Sets an additional message that will be printed during the test.

    test.comment("Test was successful");

We recommend familiarizing with the other Test methods in the documentation.

The powerful aspect of NPL tests is that they make it easy to test not only logic, but also permissions. To see this in action, however, we first need to formally introduce the concept of parties in the next sections.

Reference:

Practice

Let’s set up template test functions that we will use to test the Document protocol.

  • Navigate to the api/test/npl folder
  • Create a document-approval module within the folder
  • Within the api/test/npl/document-approval module, create a file test-document.npl
  • Within the created file, set the package to document-approval
  • Copy-paste the following template for the test function:

    @test
    function createDocument(test: Test) -> {
        test.assertTrue(true, "The test is passed");
    }
  • Similarly, create a template function editDocument:

  • Run the tests by using the npl test command

    npl test api/
Solution
package  document-approval

@test
function createDocument(test: Test) -> {
    test.assertTrue(true, "The test is passed");
}

@test
function editDocument(test: Test) -> {
    test.assertTrue(true, "The test is passed");
}

Working with protocol instances

In the previous sections, we have created a protocol, described as templates for interactions. We now need to create protocol instances to represent the actual objects that can be used and interacted with. Going from the concept of a loan and its possible data and states to a specific loan with actual data values and state at some point in time, or from the concept of a document (including approval process) to a specific document with specific content and at a specific stage of the editing/approval at some point in time. Protocol instances can be envisioned as the digital counterparts of the physical objects that can be used and interacted with.

Theory

Protocols can be instantiated to create protocol instances. Protocol instances are the actual objects that can be used and interacted with. Instances represent a specific process or object, like a specific bank account or document.

Protocol instances are created by invoking the protocol constructor. The constructor takes the parties and the constructor arguments as input.

var account = Account[account_holder, bank]("Jane Doe");

Protocol instances can be used to invoke permissions and obligations.

account.loadFunds[bank](500);

Practice

Within the test for the Document protocol, adjust the createDocument test function:

  • complete the test function to create a protocol instance
  • complete the test function to invoke the edit permission and assert that the document content has been updated
  • create a test function to approve the document and assert that the document state has been updated to the approved state.

For the time being, use the 'editor' and 'approver' (with vertical quotes) values for the parties to create the instance and invoke the permission.

Note

As we have created a protocol instance with the 'editor' and 'approver' parties, the 'editor' party will be able to edit the document and the 'approver' party will be able to approve the document.

Note

The active state of a protocol instance can be retrieved using the activeState().getOrFail() methods on an instance.

account.activeState().getOrFail()

A specific state of a protocol instance can be retrieved using the States.stateName attribute on the protocol type.

Account.States.frozen
Solution
@test
function createDocument(test: Test) -> {
    var document = Document['editor', 'approver']("Hello");
    test.assertEquals("Hello", document.content);
}

@test
function editDocument(test: Test) -> {
    var document = Document['editor', 'approver']("Hello");
    document.edit['editor']("Hello world");
    test.assertEquals(document.content, "Hello world");
}

@test
function approveDocument(test: Test) -> {
    var document = Document['editor', 'approver']("Hello");
    document.requestReview['editor']();
    document.approve['approver']();
    test.assertEquals(document.States.approved, document.activeState().getOrFail());
}

Conclusion

Congratulations, you have successfully completed the hands-on session! We hope you’ve enjoyed this guide and found it useful.

Here's a summary of what you've learned today:

  • Understand how to model a simple use case in NPL, specifically focusing on a document approval process
  • Learn to create and implement protocols with attributes, permissions, and state transitions
  • Implement permissions to enable interaction between protocol parties
  • Utilize states and state guards to control protocol workflows effectively
  • Create unit tests to verify protocol logic and permissions using the NPL testing framework
  • Gain hands-on experience in integrating parties into protocols and managing their interactions

Next steps

  • Follow the Using the NPL API with Swagger UI guide to learn how to interact with your NPL protocols via the Swagger UI
  • Extended modelling of the document approval process
  • Modelling multi-protocol workflows
  • Parties in NPL