Multi-Party Workflows
The tenzro-workflow crate is a Canton-native multi-party workflow engine that turns a Tenzro chain transaction into a structured, multi-stakeholder process: a typed lifecycle, an obligations table per counterparty, an approvals graph governed by a small policy DSL, an optional fee route, an optional privacy domain, a kill-switch path, and a hash-chained audit receipt — all mirrored into Canton's Daml runtime through the same participant node used by the Canton bridge.
Object model
A workflow is a Workflow value with the following fields:
id—WorkflowIdderived fromSHA-256("tenzro/workflow/id" || initiator || nonce_le || template_id)template_id— reference to the workflow template (e.g.AutonomousProcurement,CantonTradeSettler)initiator_did— TDIP DID of the workflow originatorcounterparties— ordered set of TDIP DIDs that must sign or act on the workflowobligations— per-counterpartyObligationrecords:{ counterparty_did, action, deadline, status }approvals—Approvalrecords gated by policy-DSL expressionsfee_route_id— optional reference to aFeeRouteused for settlement payoutsprivacy_domain_id— optional reference to aPrivacyDomainstate— typed state machine (see below)signatures— map ofdid → Signatureaccumulating multi-party consent
Lifecycle
Draft → Active → AwaitingSignatures → Executing → Completed
↘
Cancelled / Disputed / Failed / Suspended (terminal)Transitions are validated by a fixed transition table; an invalid edge returns a typed WorkflowError rather than being silently coerced.
Privileged-VM selectors
Every state-changing operation flows through a signed transaction dispatched by the Native VM, not a privileged RPC. This makes the chain's block history the canonical workflow log and lets the workflow be replayed deterministically from any node.
0x01000040—CreateWorkflow0x01000041—SubmitSignature0x01000042—CompleteObligation0x01000043—RecordApproval0x01000044—TransitionState0x01000045—RegisterFeeRoute0x01000046—RegisterPrivacyDomain0x01000047—MirrorToCanton0x01000048—KillSwitchSuspend0x01000049—KillSwitchCancel0x0100004A—OpenDispute0x0100004B—ResolveDispute
All selectors enforce signer-vs-counterparty authorization at execution; an unauthorized signer returns a typed WorkflowError::Unauthorized instead of producing a partially-applied state.
Receipts
WorkflowReceipt {
id: Hash, // SHA-256(canonical receipt bytes)
workflow_id: WorkflowId,
state_before: WorkflowState,
state_after: WorkflowState,
signer: Did,
block_height: BlockHeight,
prev_receipt: Hash, // hash-chain link; Hash::default() at genesis
payload_envelope: ReceiptEnvelope, // inline summary or DA pointer
}Receipts are persisted under wf_receipt:<id> in CF_SETTLEMENTS; the chain head is stored in the workflow's WorkflowMeta.last_receipt. The full receipt history is recovered by walking prev_receipt backwards from the head until Hash::default(). The RPC tenzro_listWorkflowReceipts walks up to max entries on demand; receipts are not held in memory.
Canton mirror
When a workflow is mirrored to Canton (selector 0x01000047), the same receipt is projected into a Tenzro.Workflow.Receipt Daml template via the co-located participant's Ledger API. The Daml template carries the ReceiptEnvelope payload either inline (defaults: settlement, kill-switch, lifecycle, governance) or as a DaPointer reference (defaults: settlement-channel, inference, agent-message), and the originating Tenzro chain's block_height + receipt_idis recorded as the cross-ledger anchor. Canton's sub-transaction privacy ensures only the workflow's stakeholders observe the mirrored receipt.
Privacy domains
A PrivacyDomain is a named ACL of TDIP DIDs that gates encrypted payloads. Workflows that opt into a domain seal their payload and event payloads with a domain key shared among the ACL. The seal/open round-trip is symmetric AES-256-GCM with the domain key derived per the standard envelope-encryption flow.
- Members in the ACL can seal and open payloads addressed to the domain.
- Auditors (a subset of the ACL) can open payloads they were never explicit recipients of.
- Frozen domains refuse new sealings while permitting existing payloads to continue being opened — preserving the data-retention contract across governance-driven freezes.
Policy DSL
Approvals are gated by a small DSL evaluated against a PolicyContext containing { now, signer, counterparties, accumulated_amount_today, kyc_tiers, ... }. Expressions evaluate to PolicyOutcome::{ Allow, Deny, RequireApproval(approver_did) }. Combinators:
amount_lte(N)/daily_amount_lte(N)— numeric ceilingscounterparty_kyc_tier_gte(tier)— KYC gatingtime_window(start, end)— wraps around midnight; supports business-hoursand(left, right)— short-circuits onDeny;RequireApprovalpropagates if either branch requiresor(left, right)— short-circuits onAllow; collapses toRequireApprovalwhen no branch allows but at least one branch requiresnot(inner)— flipsAllow ⇄ Deny;RequireApprovalis a no-op under negation
The DSL is tree-shaped, terminating, and pure — no side effects, no I/O — which makes it safe to evaluate inside the Native VM during selector dispatch.
Fee routing
FeeRoute {
id: FeeRouteId,
recipients: Vec<FeeRouteRecipient {
recipient_did: Did,
label: String,
share_bps: u16, // basis points; sum of all = 10_000
}>,
}compute_fee_route_payouts(route, gross_wei: u128) returns per-recipient payouts using basis-point splits with truncation; any rounding remainder is added to the last recipient so the sum of payouts equals the gross. The RPC tenzro_computeFeeRoutePayouts exposes this as a read-only preview; actual settlement payouts move through the consensus-mediated escrow primitive.
Kill switch
- Suspend (
0x01000048) moves the workflow intoSuspended. Callable by the initiator at any time and by governance-bound DIDs at any time. - Cancel (
0x01000049) moves aSuspendedworkflow into terminalCancelled. Callable only by the initiator after suspension.
A suspended workflow rejects all writes except KillSwitchCancel and dispute selectors. The pair removes the only condition under which an autonomous agent could be trapped in a non-responsive multi-party flow it initiated.
Operational metrics
WorkflowRuntime::operational_metrics() returns an OperationalMetrics snapshot computed by walking the in-memory workflow / obligation / approval indices once and partitioning by status. The snapshot is rendered to the standard Prometheus text format with # HELP / # TYPE headers per metric and BTreeMap ordering for deterministic output. It is exposed at /metrics and graphed by deploy/monitoring/grafana-workflow-dashboard.json (UID tenzro-workflow):
tenzro_workflow_workflows_total(gauge, labelstatus)tenzro_workflow_obligations_total(gauge, labelstatus)tenzro_workflow_approvals_total(gauge, labelstatus)tenzro_workflow_signatures_collected_total(counter)tenzro_workflow_canton_mirrored_total(counter)tenzro_workflow_fee_routes_total(gauge)tenzro_workflow_privacy_domains_total(gauge)
Surfaces
Read-only access is mirrored across all three external surfaces. Writes never occur through these surfaces — every state-changing operation is a privileged-VM selector dispatched by a signed transaction submitted via tenzro_signAndSendTransaction or eth_sendRawTransaction.
- JSON-RPC (port 8545):
tenzro_getWorkflow,tenzro_getWorkflowLifecycle,tenzro_listWorkflowsByCreator,tenzro_listWorkflowsByParticipant,tenzro_listWorkflowsByStatus,tenzro_getObligation,tenzro_getApproval,tenzro_getWorkflowReceipt,tenzro_listWorkflowReceipts,tenzro_getFeeRoute,tenzro_listFeeRoutes,tenzro_computeFeeRoutePayouts,tenzro_getPrivacyDomain,tenzro_listPrivacyDomainsForDid,tenzro_getWorkflowOperationalMetrics. - MCP (port 3001): the same surface mirrored as
#[tool]-defined methods on the main MCP server. - A2A (port 3002): the
workflowskill on the Tenzro Agent Card surfaces all of the above through natural-language utterances.
Reference templates
Five reference workflow templates ship under crates/tenzro-workflow/reference_workflows/, each paired with a *_daml_map.json describing the Canton DAML projection:
autonomous_procurement— Buyer/seller/auditor procurement on Canton with DvPautonomous_treasury— Multi-sig treasury operations with policy-gated approvalsdvp_settlement— Delivery-vs-payment settlement on a Canton synchronizerenvironmental_mrv— Environmental measurement / reporting / verification with auditor sign-offsupply_chain_dpp— Supply-chain digital product passport with multi-party attestations
Each template defines its WorkflowSpec (counterparty roles, obligation set, approvals graph, fee route, privacy domain) and is instantiated at runtime via tenzro-agent-kit's spawner. The agent-kit's separate reference_templates/ directory carries agent templates (inference marketplace, RWA custodian, bridge arbitrage scanner, etc.) that may originate workflows but are not themselves workflow specs.
See also
- Canton — the underlying enterprise-blockchain integration
- Escrow — the consensus-mediated settlement primitive
- TDIP — counterparty identity and delegation
- Tutorial: build an autonomous procurement workflow on Canton