Skip to content
bitzorcas
中EN

Guide

Workflow Notification Pipeline

Event dispatch, recipients, default templates, preferences, platform adapter, JobHost wiring, transactional failure semantics, and external delivery reconciliation.

Last updated

Workflow notification persistence now rides inside the flow command’s transactional fact chain: the platform INotificationChannel writes a per-recipient inbox entry plus a CAP Outbox row through the Notifications module, and any recipient failure fails and rolls back the entire transition. “The approval succeeded but notifications silently vanished” is no longer a legal state; cross-network external delivery (email, DingTalk, etc.) stays asynchronous under the Notifications module’s consumer — delivery confirmation belongs to that domain.

1. Dispatch chain

CAP / deliveryNotificationComposerINotificationChannelNotificationDispatcherEventDispatcherWorkflow runtimeCAP / deliveryNotificationComposerINotificationChannelNotificationDispatcherEventDispatcherWorkflow runtimelog then rethrow — transition fails and rolls backtask or lifecycle eventlistener callbackresolve recipient, template, preferenceWorkflowNotificationComposeAsync per recipient (inbox + outbox)throw on any recipient failureexternal delivery (async consumer)

EventDispatcher invokes task, execution, and workflow listeners in registration order; listener exceptions are logged and rethrown.

2. Events and recipients

EventDefault recipient
TaskCreatedtask assignee
TaskCompletedcurrent applicant
TaskRejectedcurrent applicant
TaskTransferred / Delegateddata.recipientUserIds
ParticipantAddedadded participant
InstanceCompleted/Cancelled/Terminated/Withdrawncurrent applicant
CcCreatedCC recipients
TaskRemindpending assignees

IApplicantResolver can resolve the applicant dynamically and falls back to CurrentApplicantId/StarterId. TaskCreated already creates one task per resolved user, so notifications target concrete assignees.

3. Templates

NotificationDispatcher resolves templates through IWorkflowNotificationTemplateResolver by eventType, businessType, and language; when missing or failing it falls back to built-in Chinese defaults (hard-coded in NotificationDispatcher), e.g. a title shaped like You have a new approval task: {title} in Chinese. Variables undergo controlled brace interpolation.

Template wiring through the direct builder
var engine = new WorkflowEngineBuilder()
// Only the direct Builder forwards template and preference ports to the dispatcher.
.UsePersistence(store)
.UseExpressionEvaluator(new DefaultExpressionEvaluator())
.UseNotificationChannel(channel)
.UseNotificationTemplateResolver(templateResolver)
.UseNotificationPreferenceStore(preferenceStore)
// Build validates required ports only; it provides no persistence or retry for notifications.
.Build();

4. Preferences

When the builder receives an IWorkflowNotificationPreferenceStore, recipients survive only if InboxEnabled or ExternalDeliveryEnabled is true. Preference lookup failures fail open with a log line.

NotificationService carries its own preferences and suppression rules. The two layers need one policy, otherwise the engine may drop someone the platform layer would have kept, or UI settings may diverge from actual dispatch.

5. Platform adapter

WorkflowNotificationAdapter implements the engine’s INotificationChannel on top of the Notifications module’s INotificationComposer: code workflow.{EventType}, severity mapped Urgent→Error / High→Warning / else Info, type mapping sending approval events to Approval, terminal events to Business, and CC to Todo; metadata JSON carries instanceId, businessKey, eventType, definitionKey, and officeId. Every recipient compose failure or exception increments a counter:

Current per-recipient semantics
foreach (string userId in notification.RecipientUserIds)
{
try
{
var result = await _notificationComposer.ComposeAsync(
tenantId: notification.TenantId,
userId: userId,
code: $"workflow.{notification.EventType}",
title: notification.Title,
body: notification.Body,
type: type,
category: notification.BusinessType,
severity: severity,
linkUrl: null,
linkText: null,
metadataJson: metadataJson,
cancellationToken: cancellationToken);
if (!result.IsSuccess)
{
failedRecipients++;
}
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception exception) when (exception is not OutOfMemoryException)
{
failedRecipients++;
}
}
if (failedRecipients > 0)
{
// Any recipient persistence failure escalates to a channel exception,
// letting upper layers fail and roll back the whole transition.
throw new InvalidOperationException(
$"Workflow notification persistence failed for {failedRecipients} recipient(s).");
}

Success therefore proves inbox plus Outbox writes; the trade-off is that a broken notification channel blocks approval actions themselves — monitoring the channel with transaction-grade alerting is an operational duty, not optional.

6. Node DSL notifications

NodeListenerDispatcher handles type=notification listeners by reading the __listenerRecipients variable, using channel or node-listener for Title and an expression for Body. It is a separate path from the standard NotificationDispatcher.

An empty or missing recipient list skips sending entirely (no empty notifications); any real send failure now follows the shared failure chain and fails the transition.

7. JobHost wiring

Background reminders are no longer a gap. With SqlSugar present, WorkflowBackgroundJobsExtensions.AddWorkflowBackgroundJobs registers the full chain:

  • SqlSugarWorkflowPersistenceStore and the timer tenant execution scope;
  • the timer-scoped system permission checker WorkflowTimerSystemPermissionChecker;
  • INotificationChannel → WorkflowNotificationAdapter;
  • job identity BackgroundJobIdentities.WorkflowTimer.JobName = "workflow-timer", Quartz [DisallowConcurrentExecution], a five-minute default scan overridden by BackgroundJobs:workflow-timer:IntervalSeconds.

WorkflowTimerProcessor takes ILicenseGate as a required dependency and drives due timers, timeout escalation, and timeout transfer. The execution scope commits business actions, notification Outbox rows, and Fired state atomically under tenant context. A pure in-memory host without SqlSugar registers nothing and the job never enters the descriptor catalog.

8. Failure semantics

Failure pointCurrent result
Template resolutionlog warning, use default template
Preference lookuplog warning, keep recipient
Single recipient composecounted as failure, continue others
Any failures at end of batchthrow InvalidOperationException; dispatcher rethrows; transition rolls back
Listener exceptionsEventDispatcher logs + rethrows
CAP external deliveryasynchronous Notifications consumer; Workflow does not wait

9. External delivery reconciliation boundary

Inbox plus Outbox are already atomic with the transition; cross-network delivery receipts, attempt ledgers, and DLQ reconciliation remain concerns of the Notifications consumer side. Products needing hard SLAs should add them there:

Workflow transaction

Inbox + CAP Outbox

Notifications consumer

External channels

delivery receipt / retry / DLQ

replay and reconciliation

Template versions and final rendered content should also be captured as evidence so retries do not change wording after template edits.

10. Security and privacy

Variables may contain sensitive data and NotificationDispatcher passes them into WorkflowNotification; platform Metadata selects only a few fields today, but titles/bodies can still leak through interpolation. Template allowlists, length caps, HTML/URL-context encoding, recipient tenant checks, and log redaction all need tests.

11. Test checklist

  • recipient resolution per event and no-recipient paths;
  • applicant resolver fallback;
  • missing/failing templates and preference fail-open;
  • partial multi-recipient failure rolling back the whole command;
  • identical transition replays never duplicating inbox entries;
  • JobHost reminder actually sent with atomic Fired commit;
  • CAP outage, recovery, duplicate delivery, and reconciliation;
  • template injection, oversized variables, and cross-tenant users.

Next: API reference

100%

Scroll or use controls to zoom · drag when enlarged · double-click for 100% / 200%