Workflow Actions
Every WorkflowAction type, its arguments, and how they fire on entry or exit of a state.
Reference for all EActionType values. Each action goes in either breakingAction (singular), breakingActions (plural array — used when EXECUTE_PRICING + BILL_ID_GENERATION must fire together), or nonBreakingActions (array).
NOTIFICATION
Sends email and/or SMS to the applicant. Goes in nonBreakingActions.
EApplicationContext: APPLICATION, APPLICATION_DETAILS, PAYMENT_TRANSACTION, APPLICATION_CERTIFICATES
Two subtypes based on notificationType:
STATIC — fixed HTML/SMS content
Use when message content is a fixed layout authored inline.
Locale keys: en, fr, rw
Args:
| Field | Type | Required | Notes |
|---|---|---|---|
notificationType | "STATIC" | Yes | |
staticTemplate | object | Yes | Keys: en, fr, rw |
staticTemplate[locale].smsBody | string | Yes | Plain text SMS |
staticTemplate[locale].subject | string | Yes | Email subject |
staticTemplate[locale].emailBody | string | Yes | HTML email body |
staticTemplate[locale].title | string/null | No | Defaults to null |
staticTemplate[locale].emailMessage | string/null | No | Defaults to null |
dynamicTemplate | null | Yes | Must be null for STATIC |
certificateWithList | boolean | No | Default: false |
updateFieldWithDatasetValue | boolean | No | Default: false |
excludeCurrentApplication | boolean | No | Default: true |
attachment | array/null | No | Attach files to the email — see Attachments section below |
Example:
{
"actionType": "NOTIFICATION",
"args": {
"notificationType": "STATIC",
"staticTemplate": {
"en": {
"title": null,
"subject": "Application Status",
"emailBody": "<p>Dear customer, your application has been received.</p>",
"smsBody": "Your application has been received.",
"emailMessage": null
},
"fr": {
"title": null,
"subject": "Statut de la demande",
"emailBody": "<p>Cher client, votre demande a été reçue.</p>",
"smsBody": "Votre demande a été reçue.",
"emailMessage": null
},
"rw": {
"title": null,
"subject": "Aho ubusabe bugeze",
"emailBody": "<p>Mukiriya wacu, ubusabe bwawe bwakiriwe.</p>",
"smsBody": "Ubusabe bwawe bwakiriwe.",
"emailMessage": null
}
},
"dynamicTemplate": null,
"certificateWithList": false,
"updateFieldWithDatasetValue": false,
"excludeCurrentApplication": true
}
}Attachments
Optional attachment array on any NOTIFICATION. Two source types:
DOCUMENT_GENERATION_ENGINE — certificate generated on IremboHub
Use when the certificate is generated internally by IremboHub's document generation engine. attachmentFields must be null.
"attachment": [
{
"sourceType": "DOCUMENT_GENERATION_ENGINE",
"sourceUrl": null,
"attachmentFields": null
}
]EXTERNAL_SOURCE — certificate or file fetched from an external API
Use when the attachment comes from an external integration endpoint.
How to find the correct attachmentFields:
- Call
get_integration_endpoint_mappingwith the endpoint code. - Look at
response.transform— it maps output field names to expressions. - The
attachmentFieldsare the transform keys whose values become the MinIO object name used to retrieve the file later. These keys containfileNamein their name. - Do NOT include keys ending in
-file-N— those hold base64 content, not file names.
Example — PRIME_CONTRACT_PUSH response.transform:
| Transform key | Value | Use as attachmentField? |
|---|---|---|
CERTIFICATE_FILE-file-1 | "data:application/pdf;base64,"& Document | No — base64 content |
CERTIFICATE_FILE-fileName-1 | originalPayload.serviceName & "- Contract - " & ... | Maybe — a file name |
CONTRACT-Certificate-fileName | originalPayload.serviceName & "- Contract - " & ... & ".pdf" | Yes — this is the MinIO lookup key |
→ attachmentFields: ["CONTRACT-Certificate-fileName"]
Rule: pick transform keys that contain fileName and whose value produces a .pdf / document name. This is the name the system uses to fetch the file from MinIO when sending the notification.
"attachment": [
{
"sourceType": "EXTERNAL_SOURCE",
"sourceUrl": null,
"attachmentFields": [
"CONTRACT-Certificate-fileName",
"EBM-Certificate-fileName",
"CERTIFICATE-Certificate-fileName"
]
}
]NOTIFICATION_ENGINE_TEMPLATE — pre-built templates from notification engine
Use when the notification template already exists in the notification engine.
Locale keys: en, fr, rw
Args:
| Field | Type | Required | Notes |
|---|---|---|---|
notificationType | "NOTIFICATION_ENGINE_TEMPLATE" | Yes | |
dynamicTemplate | object | Yes | Keys: en, fr, rw |
dynamicTemplate[locale].smsTemplateName | string | Yes | Template code for SMS |
dynamicTemplate[locale].emailTemplateName | string | Yes | Template code for email |
staticTemplate | null | Yes | Must be null |
attachment | array/null | No | Attach files to the email — see Attachments section above |
Example:
{
"actionType": "NOTIFICATION",
"args": {
"notificationType": "NOTIFICATION_ENGINE_TEMPLATE",
"staticTemplate": null,
"dynamicTemplate": {
"en": {
"smsTemplateName": "PAYMENT_PENDING_SMS_EN",
"emailTemplateName": "PAYMENT_PENDING_EMAIL_EN"
},
"fr": {
"smsTemplateName": "PAYMENT_PENDING_SMS_FR",
"emailTemplateName": "PAYMENT_PENDING_EMAIL_FR"
},
"rw": {
"smsTemplateName": "PAYMENT_PENDING_SMS_RW",
"emailTemplateName": "PAYMENT_PENDING_EMAIL_RW"
}
}
}
}GENERATE_CERTIFICATE
Generates a certificate for the application. Goes in breakingAction (singular).
EApplicationContext: APPLICATION, APPLICATION_DETAILS, APPLICATION_STATE_TRACKER, PAYMENT_TRANSACTION, APPLICATION_FEEDBACK, APPLICATION_CERTIFICATES
Two cases depending on where the certificate is created:
Internal — certificate template created on IremboHub
Use when the certificate template exists in IremboHub's document generation engine.
To author the template file, use generate_certificate_template — it generates a complete, print-ready FreeMarker HTML file from the service's form fields. Upload the resulting .html file in the Service Management Portal before calling list_certificate_templates. See Template Authoring for the full variable reference.
Always call list_certificate_templates(organizationId) first to look up the correct code values — never hardcode them. Each template object has:
code→ the value to use incertificateTemplateCodetemplateLanguage(en-US,fr-FR,rw-RW) → maps to the short locale key (en,fr,rw)certificateTemplateCodeis keyed by
Args (GenerateCertificateArgs):
| Field | Type | Required | Notes |
|---|---|---|---|
certificateName | string | Yes | Internal name for the certificate, e.g. "Driver_License" |
certificateTemplateCode | object | Yes | Locale keys: en, fr, rw → values are code from list_certificate_templates |
certificateExpirationDays | number/null | No | Days until certificate expires, e.g. 1825 (5 years). Null if no expiry |
reminderOffsetDays | number/null | No | Days before expiry to send a reminder notification. Null if no reminder needed |
reminderNotifications | object/null | No | Notification templates for the reminder (transient — not persisted). Same locale structure as STATIC notification |
imagesFromTheForm | object/null | No | Map of image placeholder name → form field key. Used to embed form-uploaded images into the certificate template, e.g. {"photo": "FIELD_PHOTO"} |
mergeDocuments | array/null | No | List of document field keys to merge into the certificate PDF (e.g. attachments appended to the certificate) |
certificateWithList | boolean | No | Default: false |
Example (with all optional fields):
{
"actionType": "GENERATE_CERTIFICATE",
"args": {
"certificateName": "Driver_License",
"certificateTemplateCode": {
"en": "DL_TEMPLATE_EN",
"fr": "DL_TEMPLATE_FR",
"rw": "DL_TEMPLATE_RW"
},
"certificateExpirationDays": 1825,
"reminderOffsetDays": 30,
"imagesFromTheForm": {
"applicantPhoto": "FIELD_PASSPORT_PHOTO"
},
"mergeDocuments": ["FIELD_SUPPORTING_DOCUMENT"],
"certificateWithList": false
}
}External — certificate fetched from an external API
No GENERATE_CERTIFICATE action is used. Instead, attach the certificate to an approval NOTIFICATION using attachment with sourceType: "EXTERNAL_SOURCE". The attachmentFields come from the integration endpoint's response.transform — pick the keys that contain fileName (these are the MinIO object name keys). Call get_integration_endpoint_mapping with the endpoint code to read the transform and identify them. Do not use keys ending in -file-N (those are base64 content).
{
"actionType": "NOTIFICATION",
"args": {
"notificationType": "STATIC",
"staticTemplate": { "...": "..." },
"dynamicTemplate": null,
"attachment": [
{
"sourceType": "EXTERNAL_SOURCE",
"sourceUrl": null,
"attachmentFields": ["CONTRACT-Certificate-fileName", "CERTIFICATE-Certificate-fileName"]
}
]
}
}INTEGRATION
EApplicationContext: APPLICATION, APPLICATION_DETAILS, PAYMENT_TRANSACTION, APPLICATION_FEEDBACK
Triggered during workflow transitions to send application data to a partner system — such as RDB, banks, or other government agencies. Can send application details, notifications, or completed certificates to external systems.
Always system-triggered (systemAuthorised: true, authorisedRoles: null).
Placement:
breakingAction(singular) — use when the partner system's acknowledgment is critical and the workflow must wait for a successful push before continuingnonBreakingActions(array) — use for optional updates or notifications that should not block the workflow
Dependencies:
- The partner endpoint must be active and properly configured on the integration portal
- Required application data fields must be present — missing fields may cause failures
Args:
| Field | Type | Required | Notes |
|---|---|---|---|
endpointCode | string | Yes | Code of the endpoint configured on the integration portal. Each endpoint holds the API mapping to the external partner system. Ask the user for the code — flag as TODO: describe endpoint if unknown |
async | boolean | Yes | true = workflow continues immediately without waiting for partner response; partner calls back via PUSH event. false = workflow waits until partner confirms receipt |
certificateWithList | boolean | No | Default: false |
updateFieldWithDatasetValue | boolean | No | Default: false |
excludeCurrentApplication | boolean | No | Default: true |
async: true — partner callback pattern
When async: true, the integration requires two transitions:
INITIATE_PUSH(system) →PENDING_*state withINTEGRATIONinbreakingAction,nextEvent: nullPUSH(system/partner callback) → next state withnextEvent: "INITIATE_PUSH"to continue the chain
{
"startState": "PENDING_EBM_FETCH",
"event": "INITIATE_PUSH",
"endStateOne": {
"stateName": "GENERATED_EBM",
"stateNames": null,
"stateCode": "GENERATED_EBM",
"breakingAction": {
"actionType": "INTEGRATION",
"args": {
"endpointCode": "PRIME_EBM_PUSH",
"async": true,
"certificateWithList": false,
"updateFieldWithDatasetValue": false,
"excludeCurrentApplication": true
}
},
"breakingActions": null,
"nonBreakingActions": null,
"nextEvent": null,
"position": null
},
"endStateTwo": null,
"endStateCondition": null,
"authorisation": { "authorisedRoles": null, "authorisedUsers": null, "systemAuthorised": true },
"position": null,
"eventNames": null,
"eventConfigs": null
}The PUSH callback transition that follows:
{
"startState": "GENERATED_EBM",
"event": "PUSH",
"endStateOne": {
"stateName": "Pending approval",
"stateNames": null,
"stateCode": "PENDING_APPROVAL",
"breakingAction": null,
"breakingActions": null,
"nonBreakingActions": null,
"nextEvent": "TRANSITION",
"position": null
},
"endStateTwo": null,
"endStateCondition": null,
"authorisation": { "authorisedRoles": null, "authorisedUsers": null, "systemAuthorised": true },
"position": null,
"eventNames": null,
"eventConfigs": null
}nextEvent on the PUSH callback depends on what comes next:
| What follows | nextEvent value |
|---|---|
| Another INTEGRATION in the chain | "INITIATE_PUSH" |
| Officer review or other state machine step | "TRANSITION" |
| Final state / no auto-advance | null |
async: false — synchronous
No PUSH transition needed. The integration call completes inline and the workflow auto-advances via nextEvent on endStateOne. Chain multiple sync integrations by setting nextEvent: "INITIATE_PUSH" on each — the next INITIATE_PUSH fires automatically after the sync call succeeds.
authorisation is null (not the full object) for these system-driven transitions.
Example — two chained sync integrations:
{
"startState": "SUBMITTED",
"event": "INITIATE_PUSH",
"endStateOne": {
"stateName": "Pending Push to Partner",
"stateNames": null,
"stateCode": "PUSHED_TO_PARTNER",
"breakingAction": {
"actionType": "INTEGRATION",
"args": {
"endpointCode": "RDB_MASS_ID_DBR_PUSH_2",
"async": false,
"certificateWithList": false,
"updateFieldWithDatasetValue": false
}
},
"breakingActions": null,
"nonBreakingActions": null,
"nextEvent": "INITIATE_PUSH",
"position": null
},
"endStateTwo": null,
"endStateCondition": null,
"authorisation": null,
"position": null,
"eventNames": null,
"eventConfigs": null
},
{
"startState": "PUSHED_TO_PARTNER",
"event": "INITIATE_PUSH",
"endStateOne": {
"stateName": "Pending Push to Partner",
"stateNames": null,
"stateCode": "PUSHED_TO_SAVE_NAVIGATION_CREATION",
"breakingAction": {
"actionType": "INTEGRATION",
"args": {
"endpointCode": "RDB_CREATE_NAVIGATION_IDS_DBR_PUSH",
"async": false,
"certificateWithList": false,
"updateFieldWithDatasetValue": false
}
},
"breakingActions": null,
"nonBreakingActions": null,
"nextEvent": "INITIATE_PUSH",
"position": null
},
"endStateTwo": null,
"endStateCondition": null,
"authorisation": null,
"position": null,
"eventNames": null,
"eventConfigs": null
}nextEvent on sync integrations:
| What follows | nextEvent value |
|---|---|
| Another sync integration in the chain | "INITIATE_PUSH" |
| Officer review or other state machine step | "TRANSITION" |
| Final state / no auto-advance | null |
INTEGRATION_UPDATE — retain values from integration response
Use when the integration response contains values that need to be mapped back into the application (e.g. a calculated price, an applicant name, or a reference number from an external system).
Same actionType: "INTEGRATION" — distinguished by the presence of parametersToRetain.
parametersToRetain — object where:
- key = field path in the integration response
- value = application field to write the value into
Supports nested paths, e.g. "data.amount" or "data.details[0].amount".
Placement: breakingAction or nonBreakingActions — use breaking if the workflow depends on the retained values for further transitions.
{
"actionType": "INTEGRATION",
"args": {
"endpointCode": "CODE",
"async": false,
"parametersToRetain": {
"amount": "appPrice",
"name": "applicantName"
}
}
}Chaining multiple integrations
Each integration is its own dedicated transition — never stack multiple INTEGRATION actions in one state. Chain them by setting nextEvent: "INITIATE_PUSH" on the PUSH callback's endStateOne when the next step is another integration. See Workflow Patterns for the integration-chain pattern.
BILL_ID_GENERATION
EApplicationContext: APPLICATION, PAYMENT_TRANSACTION
Generates a unique Bill ID for the application so the applicant can proceed with payment.
Triggered: system — systemAuthorised: true, authorisedRoles: null
Two placement modes depending on pricing scenario:
| Scenario | Placement |
|---|---|
| Price is fixed/known | breakingAction (singular) alone, args: null — no EXECUTE_PRICING needed |
| Price starts at 0 and will be determined later (e.g. after officer review) | breakingActions (plural array) paired with EXECUTE_PRICING at the transition where the price is finally set |
The generated Bill ID is available as ${billId} in subsequent notification templates.
{
"actionType": "BILL_ID_GENERATION",
"args": null
}Optional arg — invoiceDescription:
A FreeMarker template string rendered against the application payload and used as the payment invoice description. Pass as a key in args (not a typed DTO field — read directly from the args map):
{
"actionType": "BILL_ID_GENERATION",
"args": {
"invoiceDescription": "Payment for ${serviceName} - Application ${applicationNumber}"
}
}Always configure as breaking — the workflow cannot proceed without a successfully generated Bill ID. Optionally pair with a NOTIFICATION (nonBreakingActions) to inform the applicant of the generated bill.
This action only produces a unique payment identifier. It does not generate a certificate.
Full example — APPLY with two paths (paid vs free):
endStateOne = payment path (BILL_ID_GENERATION fires, applicant waits for PAY)
endStateTwo = free path (no bill, auto-advances to ASSIGN_OFFICE)
{
"startState": "NEW",
"event": "APPLY",
"endStateOne": {
"stateName": "Payment Pending",
"stateNames": null,
"stateCode": "PAYMENT_PENDING",
"breakingAction": {
"actionType": "BILL_ID_GENERATION",
"args": null
},
"breakingActions": null,
"nonBreakingActions": [
{
"actionType": "NOTIFICATION",
"args": {
"notificationType": "NOTIFICATION_ENGINE_TEMPLATE",
"staticTemplate": null,
"dynamicTemplate": {
"en": {
"smsTemplateName": "SVC_SUBMITTED_PAYMENT_PENDING",
"emailTemplateName": "SVC_SUBMITTED_PAYMENT_PENDING"
},
"fr": {
"smsTemplateName": "SVC_SUBMITTED_PAYMENT_PENDING",
"emailTemplateName": "SVC_SUBMITTED_PAYMENT_PENDING"
},
"rw": {
"smsTemplateName": "SVC_SUBMITTED_PAYMENT_PENDING",
"emailTemplateName": "SVC_SUBMITTED_PAYMENT_PENDING"
}
},
"certificateWithList": false
}
}
],
"nextEvent": null,
"position": null
},
"endStateTwo": {
"stateName": "Submitted",
"stateNames": null,
"stateCode": "SUBMITTED",
"breakingAction": null,
"breakingActions": null,
"nonBreakingActions": [
{
"actionType": "NOTIFICATION",
"args": {
"notificationType": "NOTIFICATION_ENGINE_TEMPLATE",
"staticTemplate": null,
"dynamicTemplate": {
"en": {
"smsTemplateName": "SVC_SUBMITTED_FREE",
"emailTemplateName": "SVC_SUBMITTED_FREE"
},
"fr": {
"smsTemplateName": "SVC_SUBMITTED_FREE",
"emailTemplateName": "SVC_SUBMITTED_FREE"
},
"rw": {
"smsTemplateName": "SVC_SUBMITTED_FREE",
"emailTemplateName": "SVC_SUBMITTED_FREE"
}
},
"certificateWithList": false
}
}
],
"nextEvent": "ASSIGN_OFFICE",
"position": null
},
"endStateCondition": null,
"authorisation": null,
"position": null,
"eventNames": null,
"eventConfigs": null
}EXECUTE_PRICING
EApplicationContext: APPLICATION, APPLICATION_DETAILS, PAYMENT_TRANSACTION, APPLICATION_FEEDBACK
Calculates the price for the application based on the configured pricing rules. Always paired with BILL_ID_GENERATION in breakingActions (plural array). args must always be null.
Placement: breakingActions (plural array) — always together with BILL_ID_GENERATION
Triggered: system — systemAuthorised: true, authorisedRoles: null
When to use:
- Price starts at 0 and is determined later (e.g. after officer review or integration) — place
EXECUTE_PRICING+BILL_ID_GENERATIONtogether inbreakingActionsat the transition where the price is finally set - Price is fixed/known — use
BILL_ID_GENERATIONalone inbreakingAction(singular); noEXECUTE_PRICINGneeded
{
"actionType": "EXECUTE_PRICING",
"args": null
}Standard PAYMENT_PENDING endState with both actions:
{
"stateName": "Payment Pending",
"stateNames": null,
"stateCode": "PAYMENT_PENDING",
"breakingAction": null,
"breakingActions": [
{ "actionType": "EXECUTE_PRICING", "args": null },
{ "actionType": "BILL_ID_GENERATION", "args": null }
],
"nonBreakingActions": [
{ "actionType": "NOTIFICATION", "args": { "...payment pending notification..." } }
],
"nextEvent": null,
"position": null
}OFFICE_ASSIGNMENT
EApplicationContext: APPLICATION, APPLICATION_DETAILS
Assigns the application to an office so it lands in the correct officer's queue. Always a breakingAction (singular). Always system-triggered (systemAuthorised: true, authorisedRoles: null).
Full args structure:
| Field | Type | Notes |
|---|---|---|
officeAssignmentType | string | See subtypes below |
officeLevel | string | LEVEL_1, LEVEL_2, LEVEL_3, etc. |
officeId | string/null | UUID — only for CODE_FIXED_OFFICE. Call list_offices to get it — never hardcode |
formFieldKey | string/null | Form field key — required for location/field-based types |
formFieldKeys | array/null | Multiple field keys — used by CODE_FROM_LOCATION when matching on several fields |
fixedOfficeCode | null | Not used — always null |
officeAssignmentExpression | null | Not used — always null |
CODE_FIXED_OFFICE
Assign to the same office for every application. Call list_offices(organisationId, level) to look up officeId — never hardcode UUIDs.
{
"actionType": "OFFICE_ASSIGNMENT",
"args": {
"officeId": "0c9984f2-9708-4566-8bb2-9a7c48b431a9",
"officeLevel": "LEVEL_1",
"formFieldKey": null,
"formFieldKeys": null,
"fixedOfficeCode": null,
"officeAssignmentType": "CODE_FIXED_OFFICE",
"officeAssignmentExpression": null
}
}CODE_FROM_LOCATION
Assign based on a location field the applicant selected. formFieldKey must end with .id. Use formFieldKeys (array) when matching on multiple location fields.
{
"actionType": "OFFICE_ASSIGNMENT",
"args": {
"officeId": null,
"officeLevel": "LEVEL_1",
"formFieldKey": "processingSector.id",
"formFieldKeys": ["processingSector.id", "embassy.id"],
"fixedOfficeCode": null,
"officeAssignmentType": "CODE_FROM_LOCATION",
"officeAssignmentExpression": null
}
}CODE_IN_APPLICATION_OFFICE
Assign based on the .value of an office field on the form (stores the office value directly).
{
"actionType": "OFFICE_ASSIGNMENT",
"args": {
"officeId": null,
"officeLevel": "LEVEL_1",
"formFieldKey": "applicationOffice.value",
"formFieldKeys": null,
"fixedOfficeCode": null,
"officeAssignmentType": "CODE_IN_APPLICATION_OFFICE",
"officeAssignmentExpression": null
}
}CODE_APPLICATION_FIELD_CODE
Assign based on the office code stored in a form field.
{
"actionType": "OFFICE_ASSIGNMENT",
"args": {
"officeId": null,
"officeLevel": "LEVEL_1",
"formFieldKey": "applicationOffice.value",
"formFieldKeys": null,
"fixedOfficeCode": null,
"officeAssignmentType": "CODE_APPLICATION_FIELD_CODE",
"officeAssignmentExpression": null
}
}
CODE_FROM_AN_EXPRESSIONexists in the enum but is not yet implemented — do not use.
OFFICE_ASSIGNMENT_NOTIFICATION
EApplicationContext: APPLICATION, APPLICATION_DETAILS, PAYMENT_TRANSACTION, APPLICATION_CERTIFICATES
Notifies the assigned officer that a new application has landed in their queue. Always paired with OFFICE_ASSIGNMENT — goes in nonBreakingActions on the same endState.
Rule: The officeAssignmentType, officeLevel, formFieldKey, and officeId in OFFICE_ASSIGNMENT_NOTIFICATION must match exactly what is in the paired OFFICE_ASSIGNMENT breakingAction.
Locale keys: en, fr, rw (same as STATIC notification)
Args:
| Field | Type | Notes |
|---|---|---|
notificationType | "STATIC" | Always STATIC for officer notifications |
staticTemplate | object | Keys: en, fr, rw — notification sent to the officer |
officeAssignmentType | string | Must match the paired OFFICE_ASSIGNMENT |
officeLevel | string | Must match the paired OFFICE_ASSIGNMENT |
formFieldKey | string/null | Must match the paired OFFICE_ASSIGNMENT |
officeId | string/null | Must match the paired OFFICE_ASSIGNMENT |
certificateWithList | boolean | Default: false |
updateFieldWithDatasetValue | boolean | Default: false |
excludeCurrentApplication | boolean | Default: true |
Example (paired with CODE_FROM_LOCATION LEVEL_1):
{
"actionType": "OFFICE_ASSIGNMENT_NOTIFICATION",
"args": {
"notificationType": "STATIC",
"staticTemplate": {
"en": {
"title": "Application Submitted",
"subject": "Application Submitted",
"emailBody": "<p>Dear Officer,<br/><br/>You have received an application for ${serviceName} with application number ${applicationNumber}, and it is pending processing!</p>",
"smsBody": "Dear Officer, you have received an application for ${serviceName} with application number ${applicationNumber}, pending processing.",
"emailMessage": null
},
"fr": {
"title": "Demande Reçue",
"subject": "Demande Reçue",
"emailBody": "<p>Cher Officer,<br/><br/>Vous avez reçu une demande pour ${serviceName} avec le numéro ${applicationNumber}, en attente de traitement!</p>",
"smsBody": "Cher Officer, vous avez reçu une demande pour ${serviceName} avec le numéro ${applicationNumber}, en attente de traitement.",
"emailMessage": null
},
"rw": {
"title": "Dosiye Yakiriwe",
"subject": "Dosiye Yakiriwe",
"emailBody": "<p>Kuri Officer,<br/><br/>Wakiriye dosiye isaba ${serviceName} ifite nomero ${applicationNumber}, itegereje gutunganywa!</p>",
"smsBody": "Kuri Officer, wakiriye dosiye isaba ${serviceName} ifite nomero ${applicationNumber}, itegereje gutunganywa.",
"emailMessage": null
}
},
"officeAssignmentType": "CODE_FROM_LOCATION",
"formFieldKey": "FIELD_CORRECTIONAL_FACILITY_NAME_DISTRICT.id",
"officeLevel": "LEVEL_1",
"officeId": null,
"certificateWithList": false,
"updateFieldWithDatasetValue": false,
"excludeCurrentApplication": true
}
}When advancing to LEVEL_2, repeat with "officeLevel": "LEVEL_2" matching the new OFFICE_ASSIGNMENT.
REDEEM_SLOT
EApplicationContext: APPLICATION, APPLICATION_DETAILS
Releases a previously reserved slot back to available inventory by adding seats back to the slot's capacity. Used on rejection transitions in services with slot booking to ensure inventory remains accurate when an application is rejected.
Not a confirmation — it is a reversal.
REDEEM_SLOTis the opposite ofSLOT_BOOKING:
SLOT_BOOKING(onAPPLY) — subtracts seats from inventory (reserves them)REDEEM_SLOT(onREJECT) — adds seats back to inventory (releases them)
Placement: breakingAction (singular) — always breaking. Place on every REJECT endState when the service uses slot booking.
Args:
| Field | Type | Notes |
|---|---|---|
groupIdField | string | Form field path holding the Group ID selected by the user |
timeRangeIdField | string | Form field path holding the Time Range ID selected by the user |
seatsField | string | Form field path holding the number of seats to release |
Example — on a REJECT transition:
{
"startState": "PAID",
"event": "REJECT",
"endStateOne": {
"stateName": "Rejected",
"stateNames": null,
"stateCode": "REJECTED",
"breakingAction": {
"actionType": "REDEEM_SLOT",
"args": {
"groupIdField": "slotbooking.group.id",
"timeRangeIdField": "slotbooking.timeRange.id",
"seatsField": "slotbooking.seats"
}
},
"breakingActions": null,
"nonBreakingActions": null,
"nextEvent": null,
"position": null
},
"endStateTwo": null,
"endStateCondition": null,
"authorisation": {
"authorisedRoles": ["ROLE_OFFICER"],
"authorisedUsers": null,
"systemAuthorised": false
},
"position": null,
"eventNames": null,
"eventConfigs": null
}Dependencies:
SLOT_BOOKINGmust have run on theAPPLYtransition (seats must have been subtracted first)- Field paths must match the form structure used in
SLOT_BOOKING
SLOT_REBOOKING
EApplicationContext: APPLICATION, APPLICATION_DETAILS
Updates the slot booking when an applicant changes their slot selection after a REQUEST_FOR_ACTION. Calls updateSlotsSeats() — replaces the previously reserved seat count with the new one from the resubmitted form. Used on the SUBMIT_RFA event transition.
Placement: breakingAction (singular) — always breaking. If the seat update fails, the resubmission cannot proceed.
Args: same structure as REDEEM_SLOT — all three fields are form field paths.
Example — on a SUBMIT_RFA transition:
{
"startState": "PENDING_RESUBMISSION_LEVEL_1",
"event": "SUBMIT_RFA",
"endStateOne": {
"stateName": "Pending Officer Processing Level 1",
"stateNames": null,
"stateCode": "PENDING_OFFICER_PROCESSING_LEVEL_1",
"breakingAction": {
"actionType": "SLOT_REBOOKING",
"args": {
"groupIdField": "slotbooking.group.id",
"timeRangeIdField": "slotbooking.timeRange.id",
"seatsField": "slotbooking.seats"
}
},
"breakingActions": null,
"nonBreakingActions": null,
"nextEvent": null,
"position": null
},
"endStateTwo": null,
"endStateCondition": null,
"authorisation": {
"authorisedRoles": ["ROLE_CITIZEN", "ROLE_AGENT"],
"authorisedUsers": null,
"systemAuthorised": false
},
"position": null,
"eventNames": null,
"eventConfigs": null
}FETCH_ID_PHOTO
EApplicationContext: APPLICATION, APPLICATION_DETAILS
Fetches the applicant's ID photo from the Identity Engine (NID or child ID), uploads it to MinIO, and saves the filename as a photo application detail entry for use in certificates or officer review.
What it does internally:
- Reads the ID type (
NIDorCHILD_ID) from thefetchValueTypeAttributefield in application details - Reads the ID number from one of the
fetchValueAttributefields - Calls the Identity Engine to fetch the base64-encoded photo
- Uploads the decoded photo to MinIO as
{idValue}_id_photo.jpg - Adds a
photoentry to application details with the stored filename
Placement: nonBreakingActions array — the workflow continues even if the photo fetch fails (failure is logged, does not block the transition). Place alongside the submission NOTIFICATION on the APPLY endState or a post-payment state.
Args:
| Field | Type | Notes |
|---|---|---|
fetchValueAttribute | array | Form field keys holding the ID number(s), e.g. ["applicantNID", "applicantChildId"]. The action tries each until a match is found |
fetchValueTypeAttribute | string | Form field key whose value is the document type (NID or CHILD_ID) |
Example — in nonBreakingActions on the APPLY endState:
{
"startState": "NEW",
"event": "APPLY",
"endStateOne": {
"stateName": "Payment Pending",
"stateNames": null,
"stateCode": "PAYMENT_PENDING",
"breakingAction": { "actionType": "BILL_ID_GENERATION", "args": null },
"breakingActions": null,
"nonBreakingActions": [
{
"actionType": "NOTIFICATION",
"args": {
"notificationType": "STATIC",
"staticTemplate": { "...": "..." },
"dynamicTemplate": null
}
},
{
"actionType": "FETCH_ID_PHOTO",
"args": {
"fetchValueAttribute": ["applicantNID", "applicantChildId"],
"fetchValueTypeAttribute": "applicantDocumentType.id"
}
}
],
"nextEvent": null,
"position": null
},
"endStateTwo": null,
"endStateCondition": null,
"authorisation": {
"authorisedRoles": ["ROLE_CITIZEN", "ROLE_AGENT"],
"authorisedUsers": null,
"systemAuthorised": false
},
"position": null,
"eventNames": null,
"eventConfigs": null
}Dependencies:
- The applicant's NID or child ID must be present in application details
- The fetched photo is stored and available as the
photodetail field for use in certificate templates
SYSTEM_REQUEST_FOR_ACTION
EApplicationContext: APPLICATION, APPLICATION_FEEDBACK
Automatically creates a feedback entry requesting the applicant to correct specific form fields or attachments. System-driven alternative to the officer-triggered REQUEST_FOR_ACTION event. Does not change the workflow state on its own — the state transition is driven by the surrounding event (typically a system TRANSITION). The applicant will be blocked from resubmitting until the flagged fields are corrected.
What it does internally:
- Inactivates any existing feedback entries on the application
- Creates a new feedback entry with the specified field names, reason, and comment
- Filters
fieldNamesto only include fields that actually exist in the service form definition
Placement: breakingAction (singular) — always breaking. Typically on a system TRANSITION event leading to a PENDING_RESUBMISSION state.
Args:
| Field | Type | Required | Notes |
|---|---|---|---|
fieldNames | array | Yes | List of form field names that need to be edited |
feedbackSection | string | No | "FORM" (default), "ATTACHMENT", or "OTHER" |
feedbackReason | string | No | Reason shown to the user. Default: "System request for field modification" |
comment | string | No | Additional comment for the user. Default: "System automatically requested field modifications" |
officerSummary | string/null | No | Optional summary for the officer |
certificateWithList | boolean | No | Default: false |
updateFieldWithDatasetValue | boolean | No | Default: false |
Example — system TRANSITION routing to PENDING_RESUBMISSION (form fields):
{
"startState": "SOME_PROCESSING_STATE",
"event": "TRANSITION",
"endStateOne": {
"stateName": "Pending Resubmission Level 1",
"stateNames": null,
"stateCode": "PENDING_RESUBMISSION_LEVEL_1",
"breakingAction": {
"actionType": "SYSTEM_REQUEST_FOR_ACTION",
"args": {
"fieldNames": ["EBM_DOCUMENT"],
"feedbackSection": "FORM",
"feedbackReason": "Request to upload EBM Invoice",
"comment": "Please upload EBM Invoice",
"certificateWithList": false,
"updateFieldWithDatasetValue": false
}
},
"breakingActions": null,
"nonBreakingActions": null,
"nextEvent": null,
"position": null
},
"endStateTwo": null,
"endStateCondition": null,
"authorisation": { "authorisedRoles": null, "authorisedUsers": null, "systemAuthorised": true },
"position": null,
"eventNames": null,
"eventConfigs": null
}Example — attachment correction:
{
"actionType": "SYSTEM_REQUEST_FOR_ACTION",
"args": {
"fieldNames": ["proofOfPublication"],
"feedbackSection": "ATTACHMENT",
"feedbackReason": "System validation failed",
"comment": "Please upload the proof of publication",
"certificateWithList": false,
"updateFieldWithDatasetValue": false
}
}TRIGGER_PARENT
EApplicationContext: APPLICATION, APPLICATION_DETAILS
Used in OSC (One Stop Center) child service workflows to fire an event on the parent workflow. When the child reaches a terminal step (payment, approval, rejection), it looks up the parent application by parentApplicationId and calls workflowService.processApplication() on it with the specified event, advancing the parent's state machine.
Placement: always breakingAction or breakingActions — must succeed for the child's transition to complete. Use singular or plural depending on whether other breaking actions accompany it.
Args (TriggerParentArgs):
| Field | Type | Required | Notes |
|---|---|---|---|
event | string | Yes | Event to trigger on the parent — "TRANSITION", "APPROVE", "REJECT", "PAY", etc. |
additionalPayload | object/null | No | Optional extra data to pass with the event to the parent workflow |
Common event mappings:
| Child event | Parent event to trigger |
|---|---|
APPROVE / APPROVE_WITH_FORM | "APPROVE" |
REJECT | "REJECT" |
PAY | "TRANSITION" |
EXPIRE_PAYMENT | "TRANSITION" |
Examples:
Officer approves child → trigger APPROVE on parent (breakingAction singular):
{
"actionType": "TRIGGER_PARENT",
"args": {
"event": "APPROVE"
}
}Officer rejects child → trigger REJECT on parent (breakingAction singular):
{
"actionType": "TRIGGER_PARENT",
"args": {
"event": "REJECT"
}
}Payment expires → trigger TRANSITION on parent (breakingActions plural, no other breaking actions):
"breakingActions": [
{
"actionType": "TRIGGER_PARENT",
"args": {
"event": "TRANSITION"
}
}
]Full transition example — EXPIRE_PAYMENT on child:
{
"startState": "PAYMENT_PENDING",
"event": "EXPIRE_PAYMENT",
"endStateOne": {
"stateName": "Payment Expired",
"stateNames": null,
"stateCode": "PAYMENT_EXPIRED",
"breakingAction": null,
"breakingActions": [
{
"actionType": "TRIGGER_PARENT",
"args": { "event": "TRANSITION" }
}
],
"nonBreakingActions": null,
"nextEvent": null,
"position": null
},
"endStateTwo": null,
"endStateCondition": null,
"authorisation": null,
"position": null,
"eventNames": null,
"eventConfigs": null
}Only use in child service workflows within an OSC setup. Do not use in standalone service workflows. Throws
NotFoundExceptionif the child application has noparentApplicationId.
APPLY_OR_RESUBMIT_CHILD_APPLICATION
EApplicationContext: APPLICATION, APPLICATION_SUMMARY, APPLICATION_DETAILS, APPLICATION_STATE_TRACKER, PAYMENT_TRANSACTION, APPLICATION_FEEDBACK, APPLICATION_CERTIFICATES
Used in OSC parent service workflows to spawn or resubmit a child application for a given sub-service. When the parent workflow reaches this action, it:
- Looks up the child service by
serviceCode - Searches for an existing child application linked to this parent (
parentApplicationId) - If none found → creates a new child application
- If one found → evaluates
childResubmitConditionsagainst the child's context; if all conditions pass, resubmits it; otherwise skips
Placement: breakingAction (singular) — must succeed for the parent to continue.
Args:
| Field | Type | Required | Notes |
|---|---|---|---|
serviceCode | string | Yes | The service code of the child service to apply for (e.g. "SRVD13F0126") |
childResubmitConditions | array/null | No | List of conditions checked against the existing child application. ALL must pass (AND logic) for a resubmit to trigger. If null, always creates new |
updateFieldWithDatasetValue | boolean | No | Default: false |
excludeCurrentApplication | boolean | No | Default: true |
certificateWithList | boolean | No | Default: false |
childResubmitConditions — each condition:
| Field | Notes |
|---|---|
parameter | Key in the child application's context — can be a form field key (from application details) or a payload key like "applicationState" |
value | Expected value to compare against |
comparator | Comparison operator — "EQ", "NEQ", etc. |
Example — spawn child, skip if condition not met:
{
"startState": "PUSHED_INITIATE_AMENDMENT_PUSH",
"event": "INITIATE_PUSH",
"endStateOne": {
"stateName": "Pushed Amend company details",
"stateNames": null,
"stateCode": "PUSHED_AMEND_COMPANY_DETAILS",
"breakingAction": {
"actionType": "APPLY_OR_RESUBMIT_CHILD_APPLICATION",
"args": {
"serviceCode": "SRVD13F0126",
"childResubmitConditions": [
{
"parameter": "applicationState",
"value": "PENDING_RESUBMISSION",
"comparator": "EQ"
}
],
"updateFieldWithDatasetValue": false,
"excludeCurrentApplication": true,
"certificateWithList": false
}
},
"breakingActions": null,
"nonBreakingActions": null,
"nextEvent": "TRANSITION",
"position": null
},
"endStateTwo": {
"stateName": "Pushed Amend company details",
"stateNames": null,
"stateCode": "PUSHED_AMEND_COMPANY_DETAILS",
"breakingAction": null,
"breakingActions": null,
"nonBreakingActions": null,
"nextEvent": "TRANSITION",
"position": null
},
"endStateCondition": {
"endStateTwo": [
{
"parameter": "DO_YOU_WANT_TO_MODIFY_COMPANY_DETAILS",
"value": "No",
"comparator": "EQ"
}
],
"validationStrategy": null,
"validationStrategyIdentifierField": null,
"childReadyStates": null,
"duplicateCheckApplicationStates": null,
"validationConfig": null
},
"authorisation": {
"authorisedRoles": null,
"authorisedUsers": null,
"systemAuthorised": true
},
"position": null,
"eventNames": null,
"eventConfigs": null
}
endStateCondition.endStateTworoutes toendStateTwo(no action, skip) when the applicant answered "No" to modifying that section.endStateOne(spawn/resubmit child) is taken when the condition is NOT met.
endStateCondition — Conditional Routing
Controls which end state a transition takes when there are two possible outcomes.
Form field routing (endStateTwo conditions)
Routes to endStateTwo when ALL listed conditions evaluate to true against the application context. If any condition fails, endStateOne is taken.
Context evaluated against: application details (form field values stored in DB) merged with the application payload (includes applicationState and other system keys). Payload values override application details.
Condition fields:
| Field | Notes |
|---|---|
parameter | Key to look up in the merged context — a form field key (e.g. "DO_YOU_WANT_TO_MODIFY_COMPANY_DETAILS") or a system key like "applicationState" |
value | Expected value |
comparator | "EQ" (equal), "NEQ" (not equal), and others supported by applicationValidationService.compareValues() |
"endStateCondition": {
"endStateTwo": [
{
"parameter": "DO_YOU_WANT_TO_MODIFY_COMPANY_DETAILS",
"value": "No",
"comparator": "EQ"
}
],
"validationStrategy": null,
"validationStrategyIdentifierField": null,
"childReadyStates": null,
"duplicateCheckApplicationStates": null,
"validationConfig": null
}Child readiness check (childReadyStates)
Used exclusively with the NOTIFY_MASTER_CHILD_READY event. Routes to endStateOne (advance) when ALL child applications are in one of the listed states; stays in endStateTwo (wait) if any child is not yet ready.
Uses countChildrenNotInStates(parentId, childReadyStates) — when count = 0, all children are ready.
"endStateCondition": {
"endStateTwo": null,
"validationStrategy": null,
"validationStrategyIdentifierField": null,
"childReadyStates": [
"PENDING_OFFICER_PROCESSING",
"REJECTED",
"APPROVED"
],
"duplicateCheckApplicationStates": null,
"validationConfig": null
}Full NOTIFY_MASTER_CHILD_READY transition:
{
"startState": "WAITING_FOR_ALL_CHILD_APPLICATIONS",
"event": "NOTIFY_MASTER_CHILD_READY",
"endStateOne": {
"stateName": "All Children Ready - Move Forward",
"stateNames": null,
"stateCode": "PUSHED_TO_PARTNER_LEVEL_1",
"breakingAction": null,
"breakingActions": null,
"nonBreakingActions": null,
"nextEvent": "INITIATE_PUSH",
"position": null
},
"endStateTwo": {
"stateName": "Child Not Ready - Wait",
"stateNames": null,
"stateCode": "WAITING_FOR_ALL_CHILD_APPLICATIONS",
"breakingAction": null,
"breakingActions": null,
"nonBreakingActions": null,
"nextEvent": null,
"position": null
},
"endStateCondition": {
"endStateTwo": null,
"validationStrategy": null,
"validationStrategyIdentifierField": null,
"childReadyStates": ["PENDING_OFFICER_PROCESSING", "REJECTED", "APPROVED"],
"duplicateCheckApplicationStates": null,
"validationConfig": null
},
"authorisation": {
"authorisedRoles": null,
"authorisedUsers": null,
"systemAuthorised": true
},
"position": null,
"eventNames": null,
"eventConfigs": null
}
endStateTwoloops back toWAITING_FOR_ALL_CHILD_APPLICATIONS(same state) withnextEvent: null— the parent simply stays put until more children complete and fireNOTIFY_MASTER_CHILD_READYagain.
OSC Parent-Child Pattern (Summary)
How the full parent ↔ child coordination works:
Child workflow responsibilities:
- Terminal transitions (APPROVE, REJECT, PAY, EXPIRE_PAYMENT) use
TRIGGER_PARENTwith the appropriateeventto signal the parent NOTIFY_MASTER_CHILD_READYis the parent event that aggregates all child completions
Parent workflow responsibilities:
- Uses
APPLY_OR_RESUBMIT_CHILD_APPLICATIONto conditionally spawn each sub-service child - Parks at
WAITING_FOR_ALL_CHILD_APPLICATIONSwithnextEvent: "NOTIFY_MASTER_CHILD_READY" - Uses
endStateCondition.childReadyStatesto define which child states count as "done" - Uses
TRIGGER_CHILD_SERVICE_EVENTto cascade events (e.g. PAY, PROCESS_PAYMENT) to all successful children
TRIGGER_CHILD_SERVICE_EVENT
EApplicationContext: APPLICATION
Used in OSC parent service workflows to fire an event on all successful child applications at once. Looks up children by parentApplicationId that have EValidationStatus.SUCCESS, then sends the specified event to each via Pulsar.
Placement: breakingAction (singular).
Args (TriggerChildEventArgs):
| Field | Type | Required | Notes |
|---|---|---|---|
childrenApplicationEvent | string | Yes | Event to trigger on each successful child — e.g. "PAY", "PROCESS_PAYMENT" |
{
"actionType": "TRIGGER_CHILD_SERVICE_EVENT",
"args": {
"childrenApplicationEvent": "PAY"
}
}Only fires on children with
SUCCESSvalidation status. Children that failed validation are skipped.
CANCEL
EApplicationContext: APPLICATION, APPLICATION_STATE_TRACKER
Cancels the application. Triggered by an officer or authorised agent via REST API (POST /cancel). Requires CANCEL_APPLICATION permission. Cannot be applied to applications already in a FINAL state.
Who triggers it: Officer / agent (HTTP API call) — NOT system-triggered.
Transition structure — typically on every non-terminal state:
{
"startState": "PENDING_OFFICER_PROCESSING",
"event": "CANCEL",
"endStateOne": {
"stateName": "Cancelled",
"stateNames": null,
"stateCode": "CANCELLED",
"breakingAction": null,
"breakingActions": null,
"nonBreakingActions": null,
"nextEvent": null,
"position": null
},
"endStateTwo": null,
"endStateCondition": null,
"authorisation": null,
"position": null,
"eventNames": null,
"eventConfigs": null
}PROCESS_BULK_APPLICATION
System event that initiates processing of a bulk master application. Loads the application context and payload, then returns the workflow result for state transition. Used at the entry point of OSC/bulk parent workflows.
Who triggers it: System (nextEvent auto-advance or background job).
No special args. No actions attached — just a state advance that sets up context for subsequent APPLY_OR_RESUBMIT_CHILD_APPLICATION chains.
VALIDATE_AND_RECORD_STATUS
System event for child applications in bulk workflows. Runs a configured validation strategy against the child application, records the result (SUCCESS/FAILED) in application_validation_state, then routes to endStateOne (passed) or endStateTwo (failed).
Who triggers it: System (nextEvent auto-advance).
Requires endStateCondition.validationStrategy — specifies which validator to run (e.g. "CONFIGURABLE", "TRAFFIC_FINE").
Routes:
endStateOne→ validation passedendStateTwo→ validation failed
{
"startState": "SUBMITTED",
"event": "VALIDATE_AND_RECORD_STATUS",
"endStateOne": {
"stateName": "Validation Passed",
"stateNames": null,
"stateCode": "VALIDATION_PASSED",
"breakingAction": null,
"breakingActions": null,
"nonBreakingActions": null,
"nextEvent": null,
"position": null
},
"endStateTwo": {
"stateName": "Validation Failed",
"stateNames": null,
"stateCode": "VALIDATION_FAILED",
"breakingAction": null,
"breakingActions": null,
"nonBreakingActions": null,
"nextEvent": null,
"position": null
},
"endStateCondition": {
"endStateTwo": null,
"validationStrategy": "CONFIGURABLE",
"validationStrategyIdentifierField": null,
"childReadyStates": null,
"duplicateCheckApplicationStates": null,
"validationConfig": null
},
"authorisation": {
"authorisedRoles": null,
"authorisedUsers": null,
"systemAuthorised": true
},
"position": null,
"eventNames": null,
"eventConfigs": null
}PROCESS_PAYMENT
System event triggered on child applications by the parent's TRIGGER_CHILD_SERVICE_EVENT action. Verifies the child's payment with the payment gateway and updates the payment transaction record (status → PAID, transaction ID, payment method, timestamp).
Who triggers it: Parent workflow via TRIGGER_CHILD_SERVICE_EVENT with childrenApplicationEvent: "PROCESS_PAYMENT".
Does not throw on failure — payment verification errors are logged but do not block the child's workflow transition.
Typical usage: After the parent PAY event, fire PROCESS_PAYMENT on all successful children to sync their payment transaction records with the gateway.