Tenzro Testnet is live —request testnet TNZO
← Back to Tutorials

Build a Multi-Party Workflow on Canton

CantonAdvanced40 min

This tutorial walks you through the full lifecycle of a Tenzro multi-party workflow — counterparty onboarding, obligation tracking, policy-gated approvals, fee-route splits, optional privacy-domain sealing, Canton receipt mirroring, and the kill-switch suspend/cancel pair — using the autonomous_procurement reference template as the worked example. The workflow is a buyer / seller / auditor procurement on Canton with DvP, but the pattern transfers verbatim to the other ten reference templates shipped under crates/tenzro-workflow/reference_workflows/.

What You'll Build

  • Three TDIP identities (buyer, seller, auditor) at appropriate KYC tiers
  • A workflow instantiated from the autonomous_procurement reference template
  • Obligation completion, policy-gated approval, and signature collection through privileged-VM selectors
  • A fee route splitting the gross payment 95% to seller, 5% to network treasury
  • An optional privacy domain sealing the workflow's payload to the three counterparties + auditor
  • Canton mirroring of the final receipt into a Tenzro.Workflow.Receipt Daml template
  • A kill-switch suspend / cancel demonstration that survives a misbehaving counterparty

Why a Workflow Engine on Top of Canton

Canton already gives you sub-transaction privacy, atomic multi-synchronizer settlement, and DAML's formally-verified contract language. What it doesn't give you natively is cross-ledger orchestration: a single object that anchors a multi-stakeholder business process to the Tenzro chain's block history while still benefiting from Canton's privacy guarantees on the side. The tenzro-workflow crate is that anchor. Every state change is a signed transaction dispatched by the Native VM through one of the workflow selectors (0x010000400x0100004B), and every transition produces a hash-chained WorkflowReceiptthat is optionally mirrored into a Daml template through the co-located Canton participant. The chain's block history is the canonical workflow log; Canton is the privacy-preserving projection.

Prerequisites. You need a running Tenzro node (local or testnet). If you don't have one yet, run tenzro join to bootstrap your identity, wallet, and hardware profile in one step. All commands below assume the node is reachable at http://localhost:8545. Canton mirroring works in graceful-degradation mode — when no Canton participant is live, the receipt is recorded on the Tenzro chain but the Daml-template projection is skipped.

Step 1: Bootstrap the Three Counterparty Identities

Procurement is a three-party flow: the buyer commits to purchasing, the seller commits to delivering, and an auditor independently signs off on the goods received. Register three TDIP human identities at KYC tier Enhanced:

tenzro identity register --type human --display-name "Buyer Co" --kyc-tier enhanced
tenzro identity register --type human --display-name "Seller Inc" --kyc-tier enhanced
tenzro identity register --type human --display-name "Auditor LLP" --kyc-tier enhanced

Each command returns a DID in the form did:tenzro:human:<uuid>. Save all three — you'll wire them into the workflow as counterparties.

Step 2: Register the Fee Route

Fee routes are static recipient tables expressed in basis points, with all shares summing to 10,000. For procurement, 95% of the gross goes to the seller and 5% goes to the network treasury:

# Build the RegisterFeeRoute transaction (selector 0x01000045) and submit it
tenzro tx send --selector 0x01000045 --data '{
  "recipients": [
    { "recipient_did": "did:tenzro:human:<seller-uuid>",  "label": "seller",   "share_bps": 9500 },
    { "recipient_did": "did:tenzro:treasury:default",     "label": "treasury", "share_bps": 500  }
  ]
}'

Once mined, the fee-route id is returned in the receipt logs. Verify it via the read-only RPC and preview a payout:

# Preview a payout for 1 TNZO (10^18 wei) gross
curl -X POST http://localhost:8545 -H "Content-Type: application/json" -d '{
  "jsonrpc":"2.0","id":1,"method":"tenzro_computeFeeRoutePayouts",
  "params":["0x<fee_route_id>", "1000000000000000000"]
}'

The response contains a payouts array with recipient_did, label, and amount_wei. Truncation rounding is added to the last recipient so the payout sum equals the gross.

Step 3: Register the Privacy Domain

Procurement payloads (line items, prices, delivery addresses) typically should be visible only to the three counterparties and the auditor. Register a privacy domain with that ACL — the auditor is in the auditor subset so it can open payloads it was never an explicit recipient of:

tenzro tx send --selector 0x01000046 --data '{
  "acl": [
    "did:tenzro:human:<buyer-uuid>",
    "did:tenzro:human:<seller-uuid>",
    "did:tenzro:human:<auditor-uuid>"
  ],
  "auditors": [
    "did:tenzro:human:<auditor-uuid>"
  ]
}'

The privacy-domain id is returned in receipt logs. AES-256-GCM seal/open round-trips against this domain key are now authorized for any DID in the ACL; non-members are rejected.

Step 4: Create the Workflow

Now wire everything together via the CreateWorkflow selector. The buyer is the initiator; the seller and auditor are counterparties. The fee route and privacy domain ids from steps 2 and 3 are bound to the workflow at creation:

tenzro tx send --selector 0x01000040 --data '{
  "template_id": "ref-autonomous-procurement",
  "initiator_did": "did:tenzro:human:<buyer-uuid>",
  "counterparties": [
    "did:tenzro:human:<seller-uuid>",
    "did:tenzro:human:<auditor-uuid>"
  ],
  "obligations": [
    { "counterparty_did": "did:tenzro:human:<seller-uuid>",  "action": "deliver_goods",  "deadline_secs": 86400 },
    { "counterparty_did": "did:tenzro:human:<auditor-uuid>", "action": "audit_delivery", "deadline_secs": 172800 }
  ],
  "fee_route_id":      "0x<fee_route_id>",
  "privacy_domain_id": "0x<privacy_domain_id>"
}'

The workflow id is returned in receipt logs. The state is now Draft. Resolve the workflow through the read RPC:

curl -X POST http://localhost:8545 -H "Content-Type: application/json" -d '{
  "jsonrpc":"2.0","id":1,"method":"tenzro_getWorkflow",
  "params":["0x<workflow_id>"]
}'

Step 5: Transition Through the Lifecycle

Move the workflow forward through Draft → Active → AwaitingSignatures → Executing → Completed by dispatching the appropriate selectors. Each transition produces a WorkflowReceipt linked to the prior chain head:

# Transition Draft  Active (initiator-only)
tenzro tx send --selector 0x01000044 --data '{ "workflow_id": "0x<id>", "to_state": "Active" }'

# Transition Active  AwaitingSignatures (initiator-only)
tenzro tx send --selector 0x01000044 --data '{ "workflow_id": "0x<id>", "to_state": "AwaitingSignatures" }'

# Each counterparty submits its signature (signs the canonical workflow hash)
tenzro tx send --selector 0x01000041 --data '{ "workflow_id": "0x<id>" }'   # signed by seller
tenzro tx send --selector 0x01000041 --data '{ "workflow_id": "0x<id>" }'   # signed by auditor

# Once all signatures are collected, transition to Executing
tenzro tx send --selector 0x01000044 --data '{ "workflow_id": "0x<id>", "to_state": "Executing" }'

At any point you can list the chain of receipts for the workflow by walking from the chain head:

curl -X POST http://localhost:8545 -H "Content-Type: application/json" -d '{
  "jsonrpc":"2.0","id":1,"method":"tenzro_listWorkflowReceipts",
  "params":["0x<workflow_id>", 100]
}'

Step 6: Complete Obligations and Record Approvals

With the workflow now Executing, the seller marks its delivery obligation as fulfilled and the auditor records the audit approval. The approval is gated by a small policy DSL — for procurement, the auditor must be at KYC tier Enhanced or higher and the audit must occur within the deadline:

# Seller marks the delivery obligation as fulfilled (signed by seller)
tenzro tx send --selector 0x01000042 --data '{
  "workflow_id":      "0x<id>",
  "counterparty_did": "did:tenzro:human:<seller-uuid>",
  "action":           "deliver_goods"
}'

# Auditor records the audit approval (signed by auditor; policy-gated)
tenzro tx send --selector 0x01000043 --data '{
  "workflow_id": "0x<id>",
  "approver_did": "did:tenzro:human:<auditor-uuid>",
  "decision":     "Approve"
}'

If the policy DSL evaluates to RequireApproval(other_did) instead of Allow, the approval is parked and a follow-up signature from the named approver is required before the workflow can advance.

Step 7: Mirror the Final Receipt to Canton

Once both obligations are completed and the auditor has approved, transition the workflow to Completed and mirror the final receipt to Canton via the co-located participant:

# Final transition to Completed
tenzro tx send --selector 0x01000044 --data '{ "workflow_id": "0x<id>", "to_state": "Completed" }'

# Mirror the latest receipt into a Tenzro.Workflow.Receipt Daml template
tenzro tx send --selector 0x01000047 --data '{ "workflow_id": "0x<id>" }'

The mirrored Daml template carries the receipt's id, state_before, state_after, signer, block_height, and the ReceiptEnvelope payload (inline for the lifecycle/governance defaults, or as a DaPointerfor the larger settlement-channel/inference defaults). Canton's sub-transaction privacy ensures only the workflow's stakeholders observe the mirrored receipt.

Step 8: Kill-Switch Demonstration

The kill switch is what makes a workflow safe to delegate to an autonomous agent. If a counterparty becomes non-responsive (the seller never delivers, the auditor never signs), the initiator can suspend the workflow at any time. Once suspended, the workflow rejects all writes except KillSwitchCancel and dispute selectors:

# Initiator suspends the workflow (always available)
tenzro tx send --selector 0x01000048 --data '{ "workflow_id": "0x<id>" }'

# After suspension, only the initiator can move it to terminal Cancelled
tenzro tx send --selector 0x01000049 --data '{ "workflow_id": "0x<id>" }'

Both transitions produce WorkflowReceipt records linked into the same hash chain, so the audit trail of why and when the workflow was killed survives indefinitely on the Tenzro chain.

Step 9: Observe Operational Metrics

Snapshot the workflow runtime's operational state:

curl -X POST http://localhost:8545 -H "Content-Type: application/json" -d '{
  "jsonrpc":"2.0","id":1,"method":"tenzro_getWorkflowOperationalMetrics",
  "params":[]
}'

# Same data in Prometheus text format
curl https://api.tenzro.network/metrics | grep tenzro_workflow_

The bundled Grafana dashboard (UID tenzro-workflow, JSON at deploy/monitoring/grafana-workflow-dashboard.json) graphs all of these — workflow / obligation / approval counts by status, signatures collected, Canton mirrors, fee routes, privacy domains.

What You Built

The same pattern transfers to the other ten reference templates under crates/tenzro-workflow/reference_workflows/ — agentic inference marketplace, autonomous RWA custodian, bridge arbitrage scanner, Canton trade settler, cross-chain liquidity aggregator, intelligent payment router, model inference proxy, MPP payment agent, multi-chain portfolio manager, and yield rebalancer. Each defines its own WorkflowSpec (counterparty roles, obligations, approvals graph, fee route, privacy domain) and is instantiated at runtime via the tenzro-agent-kit spawner.

See Also