Skip to content

Part 2: Party delegation

Thread: NPL only

Learn how NPL protocols enable controlled delegation, allowing parties to orchestrate actions on behalf of other parties through explicitly defined business logic.

Prerequisites

Learning outcomes

After completing this part, you'll be able to:

  • Understand how NPL enables controlled delegation through protocol design
  • Implement delegation patterns where one party is empowered to act on behalf of other parties, e.g. to coordinate actions by multiple other parties
  • Enable atomic multi-party workflows through protocol orchestration
  • Recognize that permissions can be called with any bound party as the calling party

Delegation in NPL

In NPL, delegation is achieved through protocol design: one protocol can be defined to call permissions on other protocols using different bound parties as the calling party. This enables the orchestration of actions across multiple protocols, with an authorization path controlled by code.

The key concept

When a permission calls another permission in NPL:

  • The permission can specify any bound party as the calling party in the permission call
  • This is controlled delegation: the protocol definition explicitly specifies which parties perform which actions
  • The delegation is encoded in the protocol's business logic, making it auditable and secure

This pattern is essential for building complex multi-party workflows where one party needs to coordinate actions across multiple protocols.

Simple delegation example

Consider a scenario where you want to automate the document approval workflow. You have two separate document instances, and you want one party to trigger the approval of both documents atomically:

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() | created {
        become inReview;
    }

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

protocol[coordinator, doc1Approver, doc2Approver] BulkApproval(
    document1: Document,
    document2: Document
) {
    @api
    permission[coordinator] approveAll() {
        // Coordinator acts as doc1Approver to approve the first document
        document1.approve[doc1Approver]();

        // Coordinator acts as doc2Approver to approve the second document
        document2.approve[doc2Approver]();
    }
}

What's happening here?

In the approveAll permission:

  1. First action: document1.approve[doc1Approver]() - The permission calls approve with doc1Approver as the calling party, delegating the approval action to that party.

  2. Second action: document2.approve[doc2Approver]() - The permission calls approve with doc2Approver as the calling party, delegating the second approval.

When someone calls approveAll[coordinator](), the permission executes using the bound parties doc1Approver and doc2Approver as calling parties for the nested permission calls. This is controlled delegation - the protocol orchestrates actions on behalf of the bound parties. The nested calls are carried out as part of the same atomic transaction triggered by the call to approveAll. They fail and the whole transaction fails if the parties bound to doc1Approver and doc2Approver do not match the parties bound to approver on document1 and document2, respectively, at the time of the call.

Why is this useful?

This pattern enables atomic multi-party transactions:

  • Both document approvals happen together
  • If one fails, neither happens (transaction atomicity)
  • The coordination logic is explicit in the protocol
  • One permission call triggers a complex multi-protocol workflow

Without delegation

Without this feature, you would need:

  1. Separate permission calls for each approval
  2. External coordination logic to ensure atomicity
  3. Complex error handling if one approval succeeds and another fails

With delegation

With delegation, the workflow is simple:

  1. One permission call to approveAll[coordinator]()
  2. NPL automatically handles both approvals atomically
  3. The protocol encodes the coordination logic

Practical example: Multi-document workflow

Let's expand the example with a complete multi-document approval scenario:

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() | created {
        become inReview;
    }

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

protocol[coordinator, doc1Approver, doc2Approver] BulkApproval(
    document1: Document,
    document2: Document
) {
    initial state pending
    final state allApproved

    @api
    permission[coordinator] approveAll() | pending {
        // Coordinator acts as doc1Approver to approve first document
        document1.approve[doc1Approver]();

        // Coordinator acts as doc2Approver to approve second document
        document2.approve[doc2Approver]();

        become allApproved;
    }
}

Creating and executing the bulk approval

// Create parties
var alice = uniquePartyOf();
var bob = uniquePartyOf();
var charlie = uniquePartyOf();
var coordinator = uniquePartyOf();

// Create two documents in review state
var doc1 = Document[alice, bob]("First document");
doc1.requestReview[alice]();

var doc2 = Document[alice, charlie]("Second document");
doc2.requestReview[alice]();

// Create bulk approval workflow
var bulkApproval = BulkApproval[coordinator, bob, charlie](doc1, doc2);

// Coordinator executes the bulk approval
// This approves BOTH documents atomically
bulkApproval.approveAll[coordinator]();

When the coordinator calls approveAll:

  1. Document 1 is approved (protocol delegates to Bob, doc1's approver ✓)
  2. Document 2 is approved (protocol delegates to Charlie, doc2's approver ✓)
  3. The bulk approval is marked as completed

Common delegation patterns

Pattern 1: Coordinator orchestrating multiple parties

A central coordinator can trigger actions by delegating to multiple parties:

protocol[coordinator, editorA, editorB] MultiDocumentEdit(
    documentA: Document,
    documentB: Document
) {
    @api
    permission[coordinator] editBoth(newContentA: Text, newContentB: Text) {
        // Protocol delegates to editorA
        documentA.edit[editorA](newContentA);

        // Protocol delegates to editorB
        documentB.edit[editorB](newContentB);
    }
}

Pattern 2: Automated workflows

A system can perform actions by delegating to authorized users:

protocol[system, approver] AutomatedApproval(
    document: Document,
    approver: Party
) {
    @api
    permission[system] autoApprove() {
        // Protocol delegates approval to the approver party
        document.approve[approver]();
    }
}

Pattern 3: Sequential workflow orchestration

An orchestrator can coordinate a multi-step workflow by delegating to different parties:

protocol[orchestrator, editor, approver] DocumentWorkflow(
    document: Document
) {
    @api
    permission[orchestrator] completeWorkflow(finalContent: Text) {
        // Protocol delegates editing to editor party
        document.edit[editor](finalContent);

        // Protocol delegates review request to editor party
        document.requestReview[editor]();

        // Protocol delegates approval to approver party
        document.approve[approver]();
    }
}

Best practice: Use delegation intentionally

  • Only delegate when it's part of the intended business logic
  • Ensure the delegating protocol has the bound parties needed for the workflow
  • Document which parties the protocol can delegate to and why
  • Remember: delegation is controlled by the protocol definition, not by the API caller

Testing delegation

Test that delegation works correctly and that authorization is enforced:

@test
function testBulkApproval(test: Test) -> {
    var alice = uniquePartyOf();
    var bob = uniquePartyOf();
    var charlie = uniquePartyOf();
    var coordinator = uniquePartyOf();

    // Create two documents in review state
    var doc1 = Document[alice, bob]("First document");
    doc1.requestReview[alice]();

    var doc2 = Document[alice, charlie]("Second document");
    doc2.requestReview[alice]();

    var bulkApproval = BulkApproval[coordinator, bob, charlie](doc1, doc2);

    // Coordinator executes the bulk approval
    bulkApproval.approveAll[coordinator]();

    // Verify both documents are approved
    test.assertEquals(
        doc1.activeState().getOrFail(),
        Document.States.approved
    );

    test.assertEquals(
        doc2.activeState().getOrFail(),
        Document.States.approved
    );

    // Verify bulk approval is completed
    test.assertEquals(
        bulkApproval.activeState().getOrFail(),
        BulkApproval.States.allApproved
    );
}

@test
function testUnauthorizedBulkApproval(test: Test) -> {
    var alice = uniquePartyOf();
    var bob = uniquePartyOf();
    var charlie = uniquePartyOf();
    var coordinator = uniquePartyOf();
    var unauthorized = uniquePartyOf();

    var doc1 = Document[alice, bob]("First document");
    doc1.requestReview[alice]();

    var doc2 = Document[alice, charlie]("Second document");
    doc2.requestReview[alice]();

    var bulkApproval = BulkApproval[coordinator, bob, charlie](doc1, doc2);

    // Unauthorized party should not be able to execute bulk approval
    test.expectFails(function() -> {
        bulkApproval.approveAll[unauthorized]();
    }, "Unauthorized party cannot execute bulk approval");
}

Key concepts recap

Concept Description
Controlled delegation Permissions can call other permissions using any bound party as the calling party
Calling party specification The syntax permission[party]() specifies which party is the calling party
Protocol orchestration One protocol coordinates actions across multiple parties/protocols
Atomic workflows Multiple delegated actions execute atomically (all succeed or all fail)

What's next?

In this part, you learned how NPL enables controlled delegation through protocol design. This enables:

  • Complex multi-party workflows orchestrated by a single protocol
  • Atomic transactions where multiple parties perform actions together
  • Coordinator patterns where one party triggers actions by others

In the next part, you'll learn about Party Assignment, which explains how real users interact with NPL protocols and how they are mapped to bound parties through authentication.

Further reading