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
- Have completed Part 1: Introduction to Parties
- Understand basic protocol structure and permissions
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:
-
First action:
document1.approve[doc1Approver]()- The permission callsapprovewithdoc1Approveras the calling party, delegating the approval action to that party. -
Second action:
document2.approve[doc2Approver]()- The permission callsapprovewithdoc2Approveras 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:
- Separate permission calls for each approval
- External coordination logic to ensure atomicity
- Complex error handling if one approval succeeds and another fails
With delegation
With delegation, the workflow is simple:
- One permission call to
approveAll[coordinator]() - NPL automatically handles both approvals atomically
- 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:
- Document 1 is approved (protocol delegates to Bob, doc1's approver ✓)
- Document 2 is approved (protocol delegates to Charlie, doc2's approver ✓)
- 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.