Issuing certificates on blockchain is a powerful way to create tamper-proof, verifiable academic or professional credentials. A traditional implementation usually requires:
- Backend servers listening to on-chain events
- Relayers or cron jobs to call issuing contracts
- Notification services to inform students
This increases infrastructure complexity and operational cost.
In this use case, we demonstrate how Kwala Workflows can fully automate the end-to-end certificate lifecycle, from student registration to certificate issuance and real-time notification, without writing any backend server logic.
Overview
The system consists of three core layers:
- Student registration: On-chain
- Certificate issuance: On-chain automation
- Student notification: Off-chain via Telegram
The entire flow is driven by smart contracts, on-chain events and YAML-based Kwala workflows. Kwala acts as the event-driven automation layer, bridging smart contracts and external systems.
The complete flow involves two smart contracts and two Kwala workflows working together to automatically issue certificates and notify students when registration occurs.
Step 1: Student registration smart contract
The StudentRegistry contract is responsible for registering students on-chain with:
- A unique student ID
- A wallet address
Once a student registers, the contract emits an event that becomes the trigger for automation.
Deployed contract address
StudentRegistry Contract: 0x2b9ab67a9ebfe5e5481d007071e38950ea29da4a
In this contract:
- Each wallet can register only once
- Each student ID is globally unique
- Registration emits an event consumed by Kwala
Smart Contract (SC1)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract StudentRegistry {
// ---- Storage ----
uint256 public totalRegistered;
mapping(address => uint256) public studentIdOf;
mapping(uint256 => address) public walletOfStudentId;
// ---- Events ----
event StudentRegistered(address indexed student, uint256 indexed studentId);
// ---- Actions ----
/// @notice Register the calling wallet with a studentId. Caller must be the student wallet.
/// @param student The wallet address of the student (must equal msg.sender)
/// @param studentId The numeric student identifier (non-zero, unique)
function register(address student, uint256 studentId) external {
require(student != address(0), "invalid student address");
require(msg.sender == student, "caller must be student");
require(studentId != 0, "studentId cannot be zero");
require(studentIdOf[student] == 0, "wallet already registered");
require(walletOfStudentId[studentId] == address(0), "studentId already taken");
studentIdOf[student] = studentId;
walletOfStudentId[studentId] = student;
totalRegistered += 1;
emit StudentRegistered(student, studentId);
}
}
Event Signature: StudentRegistered(address indexed student, uint256 indexed studentId)This event triggers the automated certificate issuance workflow.
Step 2: Certificate issuance smart Contract
The CertificateIssuer contract issues certificates only once per student and records:
- Certificate ID
- Student wallet
- Student ID
It is not called directly by users. Instead, it is invoked automatically by Kwala when a registration event occurs.
Deployed Contract Address
CertificateIssuer Contract: 0x192c91565ed652675fcd88a23e096261b5e290e1
Smart Contract (SC2)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
/// @title CertificateIssuer
/// @notice Receives student wallet and ID via relayer like Kwala and issues on-chain certificates.
contract CertificateIssuer {
uint256 public totalCertificates;
// Mapping: certificate ID => student wallet
mapping(uint256 => address) public certificateToStudent;
// Mapping: student wallet => certificate ID
mapping(address => uint256) public studentToCertificate;
event CertificateIssued(
address indexed student,
uint256 indexed studentId,
uint256 certificateId
);
/// @notice Issue a certificate to a student. Triggered externally via relayer when StudentRegistered event is emitted.
/// @param student Wallet address of the student
/// @param studentId Student's unique ID
function issueCertificate(address student, uint256 studentId) external {
require(student != address(0), "invalid address");
require(studentToCertificate[student] == 0, "already issued");
totalCertificates++;
uint256 certId = totalCertificates;
certificateToStudent[certId] = student;
studentToCertificate[student] = certId;
emit CertificateIssued(student, studentId, certId);
}
}
Important: This contract should only be called by trusted relayers like Kwala to prevent unauthorized certificate issuance.
Step 3: Workflow 1 - Auto-issuing certificates via Kwala
Automatically issue a certificate immediately after student registration.
Trigger
- Event:
StudentRegistered(address,uint256)
- Source Contract: StudentRegistry (SC1)
0x2b9AB67a9EBfe5E5481d007071E38950EA29da4a
- Chain: Polygon Amoy 80002
Actions
- CALL
issueCertificate(student, studentId) on CertificateIssuer
- POST a Telegram message indicating certificate issuance has started
Workflow YAML: Issue certificate
Name: IssueCertificateS1
Trigger:
TriggerSourceContract: 0x2b9AB67a9EBfe5E5481d007071E38950EA29da4a
TriggerChainID: 80002
TriggerEventName: StudentRegistered(address,uint256)
TriggerEventFilter: NA
TriggerSourceContractABI: W3siYW5vbnltb3VzIjpmYWxzZSwiaW5wdXRzIjpbeyJpbmRleGVkIjp0cnVlLCJpbnRlcm5hbFR5cGUiOiJhZGRyZXNzIiwibmFtZSI6InN0dWRlbnQiLCJ0eXBlIjoiYWRkcmVzcyJ9LHsiaW5kZXhlZCI6dHJ1ZSwiaW50ZXJuYWxUeXBlIjoidWludDI1NiIsIm5hbWUiOiJzdHVkZW50SWQiLCJ0eXBlIjoidWludDI1NiJ9XSwibmFtZSI6IlN0dWRlbnRSZWdpc3RlcmVkIiwidHlwZSI6ImV2ZW50In0seyJpbnB1dHMiOlt7ImludGVybmFsVHlwZSI6ImFkZHJlc3MiLCJuYW1lIjoic3R1ZGVudCIsInR5cGUiOiJhZGRyZXNzIn0seyJpbnRlcm5hbFR5cGUiOiJ1aW50MjU2IiwibmFtZSI6InN0dWRlbnRJZCIsInR5cGUiOiJ1aW50MjU2In1dLCJuYW1lIjoicmVnaXN0ZXIiLCJvdXRwdXRzIjpbXSwic3RhdGVNdXRhYmlsaXR5Ijoibm9ucGF5YWJsZSIsInR5cGUiOiJmdW5jdGlvbiJ9LHsiaW5wdXRzIjpbeyJpbnRlcm5hbFR5cGUiOiJhZGRyZXNzIiwibmFtZSI6IiIsInR5cGUiOiJhZGRyZXNzIn1dLCJuYW1lIjoic3R1ZGVudElkT2YiLCJvdXRwdXRzIjpbeyJpbnRlcm5hbFR5cGUiOiJ1aW50MjU2IiwibmFtZSI6IiIsInR5cGUiOiJ1aW50MjU2In1dLCJzdGF0ZU11dGFiaWxpdHkiOiJ2aWV3IiwidHlwZSI6ImZ1bmN0aW9uIn0seyJpbnB1dHMiOltdLCJuYW1lIjoidG90YWxSZWdpc3RlcmVkIiwib3V0cHV0cyI6W3siaW50ZXJuYWxUeXBlIjoidWludDI1NiIsIm5hbWUiOiIiLCJ0eXBlIjoidWludDI1NiJ9XSwic3RhdGVNdXRhYmlsaXR5IjoidmlldyIsInR5cGUiOiJmdW5jdGlvbiJ9LHsiaW5wdXRzIjpbeyJpbnRlcm5hbFR5cGUiOiJ1aW50MjU2IiwibmFtZSI6IiIsInR5cGUiOiJ1aW50MjU2In1dLCJuYW1lIjoid2FsbGV0T2ZTdHVkZW50SWQiLCJvdXRwdXRzIjpbeyJpbnRlcm5hbFR5cGUiOiJhZGRyZXNzIiwibmFtZSI6IiIsInR5cGUiOiJhZGRyZXNzIn1dLCJzdGF0ZU11dGFiaWxpdHkiOiJ2aWV3IiwidHlwZSI6ImZ1bmN0aW9uIn1d
TriggerPrice: NA
RecurringSourceContract: 0x2b9AB67a9EBfe5E5481d007071E38950EA29da4a
RecurringChainID: 80002
RecurringEventName: StudentRegistered(address,uint256)
RecurringEventFilter: NA
RecurringSourceContractABI: W3siYW5vbnltb3VzIjpmYWxzZSwiaW5wdXRzIjpbeyJpbmRleGVkIjp0cnVlLCJpbnRlcm5hbFR5cGUiOiJhZGRyZXNzIiwibmFtZSI6InN0dWRlbnQiLCJ0eXBlIjoiYWRkcmVzcyJ9LHsiaW5kZXhlZCI6dHJ1ZSwiaW50ZXJuYWxUeXBlIjoidWludDI1NiIsIm5hbWUiOiJzdHVkZW50SWQiLCJ0eXBlIjoidWludDI1NiJ9XSwibmFtZSI6IlN0dWRlbnRSZWdpc3RlcmVkIiwidHlwZSI6ImV2ZW50In0seyJpbnB1dHMiOlt7ImludGVybmFsVHlwZSI6ImFkZHJlc3MiLCJuYW1lIjoic3R1ZGVudCIsInR5cGUiOiJhZGRyZXNzIn0seyJpbnRlcm5hbFR5cGUiOiJ1aW50MjU2IiwibmFtZSI6InN0dWRlbnRJZCIsInR5cGUiOiJ1aW50MjU2In1dLCJuYW1lIjoicmVnaXN0ZXIiLCJvdXRwdXRzIjpbXSwic3RhdGVNdXRhYmlsaXR5Ijoibm9ucGF5YWJsZSIsInR5cGUiOiJmdW5jdGlvbiJ9LHsiaW5wdXRzIjpbeyJpbnRlcm5hbFR5cGUiOiJhZGRyZXNzIiwibmFtZSI6IiIsInR5cGUiOiJhZGRyZXNzIn1dLCJuYW1lIjoic3R1ZGVudElkT2YiLCJvdXRwdXRzIjpbeyJpbnRlcm5hbFR5cGUiOiJ1aW50MjU2IiwibmFtZSI6IiIsInR5cGUiOiJ1aW50MjU2In1dLCJzdGF0ZU11dGFiaWxpdHkiOiJ2aWV3IiwidHlwZSI6ImZ1bmN0aW9uIn0seyJpbnB1dHMiOltdLCJuYW1lIjoidG90YWxSZWdpc3RlcmVkIiwib3V0cHV0cyI6W3siaW50ZXJuYWxUeXBlIjoidWludDI1NiIsIm5hbWUiOiIiLCJ0eXBlIjoidWludDI1NiJ9XSwic3RhdGVNdXRhYmlsaXR5IjoidmlldyIsInR5cGUiOiJmdW5jdGlvbiJ9LHsiaW5wdXRzIjpbeyJpbnRlcm5hbFR5cGUiOiJ1aW50MjU2IiwibmFtZSI6IiIsInR5cGUiOiJ1aW50MjU2In1dLCJuYW1lIjoid2FsbGV0T2ZTdHVkZW50SWQiLCJvdXRwdXRzIjpbeyJpbnRlcm5hbFR5cGUiOiJhZGRyZXNzIiwibmFtZSI6IiIsInR5cGUiOiJhZGRyZXNzIn1dLCJzdGF0ZU11dGFiaWxpdHkiOiJ2aWV3IiwidHlwZSI6ImZ1bmN0aW9uIn1d
RecurringPrice: NA
RepeatEvery: event
ExecuteAfter: event
ExpiresIn: 1763541000
Meta: NA
ActionStatusNotificationPOSTURL:
ActionStatusNotificationAPIKey: NA
Actions:
- Name: call1
Type: call
APIEndpoint: NA
APIPayload:
Message: NA
TargetContract: 0x192c91565ed652675FCd88A23e096261b5e290E1
TargetFunction: function issueCertificate(address student, uint256 studentId)
TargetParams:
- re.event(0)
- re.event(1)
ChainID: 80002
EncodedABI: NA
Bytecode: NA
Metadata: NA
RetriesUntilSuccess: 5
- Name: telg1
Type: post
APIEndpoint: https://api.telegram.org/bot7754368882:AAHS4KbbOkl5rEHoBBIR8eljgwq2PARYLyY/sendMessage
APIPayload:
chat_id: 968602918
text: It's working...issuing the certificate
TargetContract: NA
TargetFunction: NA
TargetParams:
ChainID: NA
EncodedABI: NA
Bytecode: NA
Metadata: NA
RetriesUntilSuccess: 5
Execution:
Mode: parallel
Kwala extracts event parameters dynamically using re.event(index) and passes them directly to smart contract calls without backend parsing required.
re.event(0) → student address
re.event(1) → student ID
Step 4: Workflow 2 - Certificate delivery notification
Notify the student once the certificate is successfully issued on-chain.
Trigger
- Event:
CertificateIssued(address,uint256,uint256)
- Source Contract: CertificateIssuer (SC2)
0x192c91565ed652675FCd88A23e096261b5e290E1
- Chain: Polygon Amoy 80002
Action
Send a personalized Telegram message containing:
- Student ID
- Certificate ID
- Wallet address
Workflow YAML: Certificate notification
CertificateNotification.yaml
Name: certificatenotification4
Trigger:
TriggerSourceContract: 0x192c91565ed652675FCd88A23e096261b5e290E1
TriggerChainID: 80002
TriggerEventName: CertificateIssued(address,uint256,uint256)
TriggerEventFilter: NA
TriggerSourceContractABI: W3siYW5vbnltb3VzIjpmYWxzZSwiaW5wdXRzIjpbeyJpbmRleGVkIjp0cnVlLCJpbnRlcm5hbFR5cGUiOiJhZGRyZXNzIiwibmFtZSI6InN0dWRlbnQiLCJ0eXBlIjoiYWRkcmVzcyJ9LHsiaW5kZXhlZCI6dHJ1ZSwiaW50ZXJuYWxUeXBlIjoidWludDI1NiIsIm5hbWUiOiJzdHVkZW50SWQiLCJ0eXBlIjoidWludDI1NiJ9LHsiaW5kZXhlZCI6ZmFsc2UsImludGVybmFsVHlwZSI6InVpbnQyNTYiLCJuYW1lIjoiY2VydGlmaWNhdGVJZCIsInR5cGUiOiJ1aW50MjU2In1dLCJuYW1lIjoiQ2VydGlmaWNhdGVJc3N1ZWQiLCJ0eXBlIjoiZXZlbnQifSx7ImlucHV0cyI6W3siaW50ZXJuYWxUeXBlIjoidWludDI1NiIsIm5hbWUiOiIiLCJ0eXBlIjoidWludDI1NiJ9XSwibmFtZSI6ImNlcnRpZmljYXRlVG9TdHVkZW50Iiwib3V0cHV0cyI6W3siaW50ZXJuYWxUeXBlIjoiYWRkcmVzcyIsIm5hbWUiOiIiLCJ0eXBlIjoiYWRkcmVzcyJ9XSwic3RhdGVNdXRhYmlsaXR5IjoidmlldyIsInR5cGUiOiJmdW5jdGlvbiJ9LHsiaW5wdXRzIjpbeyJpbnRlcm5hbFR5cGUiOiJhZGRyZXNzIiwibmFtZSI6InN0dWRlbnQiLCJ0eXBlIjoiYWRkcmVzcyJ9LHsiaW50ZXJuYWxUeXBlIjoidWludDI1NiIsIm5hbWUiOiJzdHVkZW50SWQiLCJ0eXBlIjoidWludDI1NiJ9XSwibmFtZSI6Imlzc3VlQ2VydGlmaWNhdGUiLCJvdXRwdXRzIjpbXSwic3RhdGVNdXRhYmlsaXR5Ijoibm9ucGF5YWJsZSIsInR5cGUiOiJmdW5jdGlvbiJ9LHsiaW5wdXRzIjpbeyJpbnRlcm5hbFR5cGUiOiJhZGRyZXNzIiwibmFtZSI6IiIsInR5cGUiOiJhZGRyZXNzIn1dLCJuYW1lIjoic3R1ZGVudFRvQ2VydGlmaWNhdGUiLCJvdXRwdXRzIjpbeyJpbnRlcm5hbFR5cGUiOiJ1aW50MjU2IiwibmFtZSI6IiIsInR5cGUiOiJ1aW50MjU2In1dLCJzdGF0ZU11dGFiaWxpdHkiOiJ2aWV3IiwidHlwZSI6ImZ1bmN0aW9uIn0seyJpbnB1dHMiOltdLCJuYW1lIjoidG90YWxDZXJ0aWZpY2F0ZXMiLCJvdXRwdXRzIjpbeyJpbnRlcm5hbFR5cGUiOiJ1aW50MjU2IiwibmFtZSI6IiIsInR5cGUiOiJ1aW50MjU2In1dLCJzdGF0ZU11dGFiaWxpdHkiOiJ2aWV3IiwidHlwZSI6ImZ1bmN0aW9uIn1d
TriggerPrice: NA
RecurringSourceContract: 0x192c91565ed652675FCd88A23e096261b5e290E1
RecurringChainID: 80002
RecurringEventName: CertificateIssued(address,uint256,uint256)
RecurringEventFilter: NA
RecurringSourceContractABI: W3siYW5vbnltb3VzIjpmYWxzZSwiaW5wdXRzIjpbeyJpbmRleGVkIjp0cnVlLCJpbnRlcm5hbFR5cGUiOiJhZGRyZXNzIiwibmFtZSI6InN0dWRlbnQiLCJ0eXBlIjoiYWRkcmVzcyJ9LHsiaW5kZXhlZCI6dHJ1ZSwiaW50ZXJuYWxUeXBlIjoidWludDI1NiIsIm5hbWUiOiJzdHVkZW50SWQiLCJ0eXBlIjoidWludDI1NiJ9LHsiaW5kZXhlZCI6ZmFsc2UsImludGVybmFsVHlwZSI6InVpbnQyNTYiLCJuYW1lIjoiY2VydGlmaWNhdGVJZCIsInR5cGUiOiJ1aW50MjU2In1dLCJuYW1lIjoiQ2VydGlmaWNhdGVJc3N1ZWQiLCJ0eXBlIjoiZXZlbnQifSx7ImlucHV0cyI6W3siaW50ZXJuYWxUeXBlIjoidWludDI1NiIsIm5hbWUiOiIiLCJ0eXBlIjoidWludDI1NiJ9XSwibmFtZSI6ImNlcnRpZmljYXRlVG9TdHVkZW50Iiwib3V0cHV0cyI6W3siaW50ZXJuYWxUeXBlIjoiYWRkcmVzcyIsIm5hbWUiOiIiLCJ0eXBlIjoiYWRkcmVzcyJ9XSwic3RhdGVNdXRhYmlsaXR5IjoidmlldyIsInR5cGUiOiJmdW5jdGlvbiJ9LHsiaW5wdXRzIjpbeyJpbnRlcm5hbFR5cGUiOiJhZGRyZXNzIiwibmFtZSI6InN0dWRlbnQiLCJ0eXBlIjoiYWRkcmVzcyJ9LHsiaW50ZXJuYWxUeXBlIjoidWludDI1NiIsIm5hbWUiOiJzdHVkZW50SWQiLCJ0eXBlIjoidWludDI1NiJ9XSwibmFtZSI6Imlzc3VlQ2VydGlmaWNhdGUiLCJvdXRwdXRzIjpbXSwic3RhdGVNdXRhYmlsaXR5Ijoibm9ucGF5YWJsZSIsInR5cGUiOiJmdW5jdGlvbiJ9LHsiaW5wdXRzIjpbeyJpbnRlcm5hbFR5cGUiOiJhZGRyZXNzIiwibmFtZSI6IiIsInR5cGUiOiJhZGRyZXNzIn1dLCJuYW1lIjoic3R1ZGVudFRvQ2VydGlmaWNhdGUiLCJvdXRwdXRzIjpbeyJpbnRlcm5hbFR5cGUiOiJ1aW50MjU2IiwibmFtZSI6IiIsInR5cGUiOiJ1aW50MjU2In1dLCJzdGF0ZU11dGFiaWxpdHkiOiJ2aWV3IiwidHlwZSI6ImZ1bmN0aW9uIn0seyJpbnB1dHMiOltdLCJuYW1lIjoidG90YWxDZXJ0aWZpY2F0ZXMiLCJvdXRwdXRzIjpbeyJpbnRlcm5hbFR5cGUiOiJ1aW50MjU2IiwibmFtZSI6IiIsInR5cGUiOiJ1aW50MjU2In1dLCJzdGF0ZU11dGFiaWxpdHkiOiJ2aWV3IiwidHlwZSI6ImZ1bmN0aW9uIn1d
RecurringPrice: NA
RepeatEvery: event
ExecuteAfter: event
ExpiresIn: 1762957800
Meta: NA
ActionStatusNotificationPOSTURL:
ActionStatusNotificationAPIKey: NA
Actions:
- Name: telegramNoti1
Type: post
APIEndpoint: https://api.telegram.org/bot7754368882:AAHS4KbbOkl5rEHoBBIR8eljgwq2PARYLyY/sendMessage
APIPayload:
chat_id: '968602918'
text: Hi re.event(1), your certificate ID re.event(2) is credited on your wallet address re.event(0).
TargetContract: NA
TargetFunction: NA
TargetParams:
ChainID: NA
EncodedABI: NA
Bytecode: NA
Metadata: NA
RetriesUntilSuccess: 5
Execution:
Mode: parallel
API payload
{
"chat_id": "968602918",
"text": "Hi re.event(1), your certificate ID re.event(2) is credited on your wallet address re.event(0)."
}
Dynamic Parameters:
re.event(0) → student wallet address
re.event(1) → student ID
re.event(2) → certificate ID
Kwala automatically substitutes these placeholders with actual event data at runtime.
Complete flow summary
The certificate issuance workflow follows these four steps:
- Step 1: Registration - Student calls register() on StudentRegistry contract
- Step 2: Event emitted - StudentRegistered event triggers Workflow 1
- Step 3: Certificate issued - Kwala calls issueCertificate() on CertificateIssuer
- Step 4: Notification - CertificateIssued event triggers Workflow 2 and Telegram notification sent
This architecture can be extended for:
- Academic credentials - Degrees, diplomas, and course completions
- Professional badges - Skill certifications and training credentials
- Government IDs - Verifiable identity documents and licenses
- Enterprise credentials - Employee certifications and compliance records
Conclusion
This use case demonstrates how Kwala Workflows can be used to build a production-ready, automated certificate issuance system using solidity smart contracts, on-chain events and YAML workflow definitions.
By combining StudentRegistry, CertificateIssuer, and Kwala, institutions can issue verifiable certificates at scale securely, transparently, and without infrastructure overhead. Kwala handles the automation. Blockchain guarantees trust.
Next steps