Notification Rules
The Notification Rules screen is the write side of the notification system — a library of notification-rule resources that decide:
- when a notification fires (status code and / or rejection reason);
- what it says (subject and body, or the dispatcher defaults);
- to whom (a portal user / role, an email list, or both);
- on which channels (portal inbox, e-mail, external API call), with the rendered PDF attached to the e-mail by default.
Rules are matched by InvoiceStatusCatalog.StatusTransition.apply() after the database write of every status change, by the manual SetStatusModal, and by the CLI flows (-process, -fetch-import, -fetch-status, -fetch-single, -fetch-all). A failure in dispatch never aborts the underlying status update.
The read side of the system — the inbox where users acknowledge notifications and the navbar bell — is documented on the Notifications page.
The page applies regardless of source system — JD Edwards, SAP, NetSuite or a custom ERP. Triggers reference the standard statuses and rejection-reason-codes catalogues, not source-system-specific codes.
Three changes shipped in 2026.05.7:
- Six-tab editor — the long single form is split into General · Trigger · Channels · Email · Actions · Test. Each tab fits a screen and the action list has room to grow.
- Multi-call action list — the Actions tab now holds a list of connector calls instead of a single call. Each call carries a free-form Description, an optional Stop on failure flag, and the API or SQL connector + target dropdown. Calls fire in declaration order, with response chaining via
{call.N.fieldName}placeholders. - SQL connector support — the connector dropdown lists API and SQL Connectors merged with
· API/· SQLsuffixes. The target dropdown loads endpoints or queries depending on the picked connector kind.
Three dispatcher bugs were also fixed at the same time: the action channel no longer needs the legacy action checkbox to fire, an empty recipient list no longer suppresses action-only rules, and a hung SMTP transport.close() can no longer block the chain. See the 2026.05.7 release notes for the full list.
Opening the editor
- Sidebar → Management → Notification Rules.
- A fresh installation ships with no rules — the dispatcher emits nothing until the first rule is created. Use Add to create one.
At a glance
The page is split between the rule list on the left and the rule editor on the right. Each rule is a notification-rule resource saved in config-notifications.json; the dispatcher reloads them at startup and after every save.
Toolbar actions
| Action | Effect |
|---|---|
| Add | Opens a modal asking for a name and a description. Creates a new rule in disabled-when-empty state — no trigger, channels defaulted to portal, recipient type defaulted to user. |
| Copy | Duplicates the selected rule under a new name. Convenient for deriving a B2C variant from a B2B rule, or a per-region rule from a generic one. |
| Remove | Deletes the selected rule after a confirmation. |
| Save | Writes the editor state back to config-notifications.json and signals the dispatcher to reload. The next status change picks up the new rule. |
The rule list
Every rule in the catalogue appears with two visual cues:
- Description in italic underneath the rule name — the same field the Add modal asked for, free text.
on/offpill at the right of each row — bound to theenabledproperty. A rule withoffstays in the catalogue but is skipped at dispatch time. Useful while iterating on a rule without losing it.
A search box at the top of the sidebar filters the list by substring on the rule name.
The rule editor
In 2026.05.7 the editor was reorganised into six tabs — each tab fits a screen and the action list has room to grow. The tab resets to General when you switch rules.
General tab
| Field | Description |
|---|---|
| Description | Free-form sentence shown under the rule name in the rule list. The first place to read what the rule does. |
| Enabled | Single checkbox. When off, the rule stays in the catalogue but is skipped at dispatch time. The same value drives the on / off pill on the rule list. |
Trigger tab
The trigger decides when the rule fires. Three fields combine, all optional:
| Field | Source | Behaviour |
|---|---|---|
| Status codes | statuses catalogue | Chip multi-select. The rule fires when the status code of the new transition is in this list. Empty = match every status. |
| Reason codes | rejection-reason-codes catalogue | Chip multi-select. The rule fires only when the reason code of the new transition is in this list. Empty = match every reason. |
| Direction | Any (default) / Issued only (sales) / Received only (purchases) | Picks which side of the workflow the rule applies to. Any keeps the rule active on both directions. Issued only skips the rule on supplier-received invoices; Received only skips it on customer-issued ones. Evaluated against the UHDRIN flag stored on the row (see Invoices → Direction chip). |
Both fields are surfaced as chip multi-selects — picking from a dropdown adds a chip; the × on a chip removes it. The dropdown is populated from the same statuses and rejection-reason-codes resources used by the Set Status modal and the invoice History tab, so a rule cannot reference a code that the application does not recognise.
When several fields are filled, they all must match (logical AND) for the rule to fire — so a Status = 207, Direction = Received only rule wires the supplier-side rejection notification without colliding with the customer-side Status = 207 rule that already exists on the same template.
Channels tab
Two delivery channels, plus the recipient settings:
portal— writes a row inF564253for the recipient. The user sees it in the Notifications inbox and the bell.email— sends an SMTP message via the configurede-invoicingmail account.
The legacy action checkbox is gone since 2026.05.7 — action calls fire automatically when the rule has at least one entry in the Actions tab. To suppress action calls, disable the rule on the General tab.
The recipient model has two independent halves: a portal target (user / role) and an email list.
| Field | Description |
|---|---|
| Portal target | Picks the portal target — User (by username), Role, or empty. When auth is disabled, the empty option reads Broadcast — all portal users and writes a single F564253 row under the * sentinel. |
| Username / Role name | The username or role name selected by Portal target. Free text — auto-completion comes from the connected database when available. |
| Email list | Independent list of e-mail addresses, separated by , or ;. Each address gets its own email (no shared To: header). Independent of the portal target above. |
When auth is enabled, a portal target with an email on its F564250 row also receives the email channel automatically.
Email tab
The email tab shows its content unconditionally — no checkbox gates it any more. Whether the rule actually sends an email depends on the Channels tab's email checkbox.
| Field | Default | Description |
|---|---|---|
| Subject | Invoice {doc} {dct} {kco} — {statusLabel} | Subject line. Placeholders are resolved at dispatch time. |
| Body | Status: {statusLabel}\nReason: {reasonLabel}\nAction: {actionLabel}\n{message} | Plain-text body. Multi-line input. |
| Attach PDF | Y | Render the invoice PDF (via the resolved pdf-template) and attach it to the e-mail. The PDF is rendered once per dispatch and reused across every recipient; a render failure is logged but never fails the e-mail. |
Available placeholders in subject / body: the 10 canonical notification fields — {doc}, {dct}, {kco}, {statusCode}, {statusLabel}, {statusMessage}, {reasonCode}, {reasonLabel}, {actionCode}, {actionLabel}, {ruleName}, {message} — plus {call.N.fieldName} for outputs of earlier action calls in this rule (see Actions tab below).
Since 2026.05.14 the placeholder set also merges every column from the Invoices view's catalog — {customerName}, {contractRef}, {totalHT}, {currency}, {logBusinessUnit}, {logPaUuid}, … — pulled in a single F564231 LEFT JOIN F564230 SELECT alongside the canonical fields. Amounts are surfaced with two decimals, JDE Julian dates as ISO, CHAR columns trimmed. Canonical fields take precedence when a catalog column happens to share a name, so {doc} / {status} keep their familiar meaning.
A { } button sits next to the Subject input and the Body textarea. Click it to open a searchable picker of every available placeholder; pick one and the {name} snippet splices into the field at the caret (or appends with a leading space when the input is not focused). Escape / outside-click closes.
The portal NTSUBJ uses the same subject; the portal NTMSGE defaults to just {statusLabel} because the inbox UI already shows the doc reference inline — duplicating it in the body would be noise.
Actions tab
The Actions tab holds a list of connector calls instead of a single call. Each call is rendered as a collapsible card. The card header shows the call index (#1, #2, …) and either the Description field or connector · target when no description is set; clicking it expands the editor.
| Field | Description |
|---|---|
| Description | Free-form short label for the call (e.g. Update CRM customer status). Rendered as the collapsed-card header so a binding with several calls reads as a punch-list at a glance. Persisted only as UI metadata. |
| Connector | Dropdown listing every api-connector and sql-connector, merged with · API / · SQL suffixes so the kind is visible. |
| Target | Dropdown loaded from api.connectors.listEndpoints(connector) for an API connector, or api.sqlConnectors.listQueries(connector) for a SQL connector. Disabled until a connector is picked. |
| Parameters | One row per parameter declared by the target, plus an Add parameter button for ad-hoc keys. Values may contain {placeholders} — see below. |
| On failure | Single checkbox. When ticked, a failure on this call halts the chain and skips every remaining call (STOP · N remaining call(s) skipped in the audit trail). Default off — the chain continues by default, mirroring the email channel's continue-on-error semantics. |
+ Add Call at the bottom of the list appends a new call. New calls auto-expand; loading a rule collapses everything.
Placeholders
Parameter values support the same {placeholders} as the email subject / body — the 10 canonical fields, every Invoices catalog column merged in 2026.05.14, and the {call.N.fieldName} form that references the output of an earlier call in the same rule. Each parameter row carries its own { } picker.
Response chaining
Every call's outputs are folded back into the dispatch context under call.N.* keys, where N is the 1-based index of the call within the rule. Subsequent calls reference them as {call.N.fieldName} in their parameter values.
| Field | Source — API connector call | Source — SQL connector call |
|---|---|---|
call.N.success | true when HTTP status < 400. | true when the statement ran without error. |
call.N.statusCode | HTTP status code returned by the endpoint. | — |
call.N.statementType | — | SELECT / INSERT / UPDATE / DELETE / MERGE. |
call.N.rowCount | — | For SELECT — number of rows returned. |
call.N.updateCount | — | For non-SELECT — number of rows affected. |
call.N.error | Error message when success is false. | Same. |
call.N.<name> | Every endpoint.N.response.<name> mapping the connector defines. | Every column of the first row of the result, by name. |
Example: a rule that first reads the customer's email via a SQL query, then sends a follow-up HTTP webhook, would set the webhook payload's to parameter to {call.1.EMAIL} (the EMAIL column of the first row of call #1).
Action calls fire before email
Action calls now run before the email channel, so a hung SMTP transport.close() can never block them. The portal write still goes first; emails follow once the action chain has run.
Test tab
A synchronous Test runner. It accepts a (doc, dct, kco) triplet, optionally a status code and a custom message, and actually fires the rule through every enabled channel — the portal write lands in the inbox, the action chain executes, the e-mail goes out via SMTP. The resulting banner reports the dispatch counts (✓ Dispatched · 1 portal · 2 email · 3 action(s) ran) or the first error.
The test panel does not save the rule — only fires whatever is currently in the form. Use it to validate edits before clicking Save.
How a notification gets dispatched
When a status transition is applied (every database write that touches F564231.UHK74RSCD), the dispatcher walks the catalogue in three steps.
StatusTransition.apply()
↓
NotificationDispatcher.fire(doc, dct, kco, status, reason, action, message)
↓ — for each rule where enabled = Y
↓ match trigger.status (CSV) ∋ status
↓ AND match trigger.reason (CSV) ∋ reason (or trigger.reason = '')
↓ → resolve recipient (portal target + email list)
↓ → render the invoice PDF once if attachPdf = Y
↓ → run the action chain (before email, so a hung SMTP can't block it)
↓ for each call N in declaration order:
↓ api-action → HTTP call to connector.endpoint with resolved params
↓ sql-action → JDBC call to connector.query with bound :params
↓ outputs folded into context under call.N.*
↓ if call failed AND stopOnFailure → STOP · skip remaining
↓ else continue (continue-on-error default)
↓ append per-call audit footer to NTK74MSG2
↓ → portal → INSERT into F564253 (with audit footer)
↓ → email → SMTP one message per recipient (transport.close bounded 5s)
↓ all exceptions caught — failures never abort the status update
The dispatcher uses a singleton with a 2-thread asynchronous worker pool, so the calling code returns immediately. A JVM shutdown hook drains the pool with a 2-second grace before the process dies, so CLI flows that exit right after a status update still deliver their notifications.
Every dispatch emits structured INFO lines on stdout: dispatch rule=X status=Y channels=… actions=N, then dispatching N action call(s), then call #N → connector/endpoint, then api-action … HTTP 200 / sql-action … ok (N row(s)). WARN lines mark stop-on-failure hits and slow SMTP closes; ERROR lines carry the failure reason. The same audit lines feed the colour-coded chips on the Notifications inbox.
REST API
The page reads and writes via the standard template endpoints; the dispatcher exposes one extra route for the test panel.
| Method + path | Purpose |
|---|---|
GET /api/templates | Lists all templates; the page filters by type = notification-rule. |
GET /api/templates/{name} | Loads a single rule. |
POST /api/templates | Creates a new rule (Add). |
POST /api/templates/{from}/copy/{to} | Duplicates (Copy). |
PUT /api/templates/{name} | Saves edits. |
DELETE /api/templates/{name} | Removes a rule. |
POST /api/notifications/test | Fires the rule's payload synchronously against every enabled channel — used by the Test panel. |
Tips & best practices
- Start narrow, widen later. A trigger of
9904 + REJ_ADRis easier to validate than a catch-all'', and you keep the noise low while the recipient list and the body are still being tuned. - Use the Test panel before saving. Especially for the action channel — the dispatcher swallows failures, so a misconfigured connector silently no-ops at production time. The test runner surfaces the same error inline.
- Keep one rule per purpose, not per status code. Group several status codes behind a single rule when the body is identical (
9904, 9907 → Rejected); split into separate rules only when the recipient list or the body differs. - PDFs are heavy.
attachPdfrenders the invoice once per dispatch — fine for low-volume rules, expensive for fleet-wide alerts. Disable it on rules that fire on9900(just-created) or9901(validated), where the PDF rarely adds value. - Use
roleoveruserwhenever possible. A role-based recipient survives staff changes; auser-based one needs an edit each time the assignee leaves. The role list onF564251is the source of truth. - Disable, don't delete. While iterating, flip the rule
offinstead of removing it — the catalogue keeps the history, the dispatcher skips it, and the test runner remains available. - Read the inbox after a release. Rules sometimes drift from the codes they reference (a status renamed in the catalogue, a reason retired) — the Notifications page is the fastest cross-check that the production catalogue is still consistent with the rules in this page.