Configuration
All properties bind under the notify4j prefix (NotificationProperties).
1. Properties
| Property | Default | Description |
|---|---|---|
|
(empty) |
Apprise/shoutrrr-style channel URLs (see Channel URLs). Blank entries are skipped. |
|
|
Status transitions to suppress on every channel (see Transition filtering). |
|
|
Add an always-on logging sink. Handy as a no-config default. |
|
|
Build the single app-wide facade. Set |
|
(empty) |
Email recipients. The email channel is inactive until this is set. |
|
(spring.mail default) |
From address. |
|
|
Prepended to the subject line. |
|
|
Connection timeout for the webhook-style channels. |
|
|
Read (per-request) timeout for the webhook-style channels. |
|
|
Delivery attempts per channel on transient failures (HTTP 5xx/429, |
|
|
Base backoff between retries; doubled each attempt, capped at 30s. |
|
|
Deliver off the caller thread on a shared pool, so a slow channel can’t block the caller
or its siblings. Set |
|
|
Size of the shared async delivery thread pool. |
|
|
Bound on the delivery queue, so a burst or a persistently-slow channel can’t grow it without limit and OOM the host. |
|
|
What to do when the pool and queue are both full. |
|
|
Re-notify every channel for an entity that stays in a reminder status until it resolves. |
|
(empty) |
Statuses that arm a reminder (e.g. |
|
|
How long an entity must stay in a reminder status before it is re-notified. |
|
|
How often the scheduler checks for due reminders. |
A single HttpClient is shared across all webhook-style channels (Slack, Teams, Discord,
webhook, Telegram, ntfy, PagerDuty, OpsGenie); the timeouts and retry policy above apply to
it.
By default the starter delivers asynchronously: send returns immediately and each channel
fires concurrently on the shared pool, retrying transient HTTP failures. The pool is shut
down with the application context.
Email is the one channel that is not a URL: it is Spring-mail-coupled and its SMTP
transport comes from the standard spring.mail.* properties. It activates only when a
JavaMailSender is present and notify4j.email.to is set.
2. Channel URLs
A channel URL is scheme[+transport]://endpoint[?tags=a,b]:
-
scheme selects the channel (and therefore the payload shape).
-
transport (
+http/+https) selects the wire protocol; defaulthttps.httpis mainly for tests. -
?tags=restricts which events reach the channel (see Tag routing); it is stripped before the endpoint is built.
| Scheme | Example |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Teams: Microsoft retired the legacy Office 365 connector incoming webhooks in May 2026. Create a Power Automate "Workflows" webhook (e.g. the "Post to a channel when a webhook request is received" / "Send webhook alert to a channel" template) and use that URL. notify4j posts a MessageCard, which the Workflows endpoint accepts. |
3. Tag routing
A channel with no tags fires for every event. A tagged channel fires only when the
event is sent with at least one overlapping tag:
notify4j:
urls:
- slack://... # untagged -> every event
- pagerduty://<key>?tags=failed # only events routed with the "failed" tag
notifications.send(event); // Slack only
notifications.send(event, List.of("failed")); // Slack + PagerDuty
4. Transition filtering
notify4j tracks the last status per event id and delivers only on a change. Patterns in
ignore-changes suppress specific transitions, written OLD:NEW with * wildcards:
-
*:RUNNING— never notify when something entersRUNNING. -
SUCCESS:FAILED— suppress exactly this transition.
The defaults (:PENDING, :RUNNING, *:ASSIGNED) mean channels fire on terminal
SUCCESS/FAILED rather than on intermediate states.
5. Runtime mute
Beyond static ignore-changes, you can mute at runtime via the facade — e.g. silence one
noisy subject for an hour — and list or remove active mutes:
notifications.addFilter(filter); // a NotificationFilter (e.g. ExpiringFilter)
notifications.filters(); // active, non-expired mutes
notifications.removeFilter(id);
6. Metrics
When micrometer-core is on the classpath and a MeterRegistry bean exists (e.g. with Spring
Boot Actuator), the starter records a counter per channel and outcome — no configuration
needed:
| Meter | Tags |
|---|---|
|
|
sent = delivered, failed = gave up after retries, suppressed = filtered out (e.g. not a
real transition). The core itself has no metrics dependency; it records against a
NotificationMetrics SPI that defaults to no-op when no registry is present.
7. Multi-tenant: async, retry & metrics without the starter
The notify4j. properties above only configure the starter’s single, app-wide facade.
A *multi-tenant app instead injects the NotificationsFactory (and sets
notify4j.global=false) to build one facade per tenant from that tenant’s URLs. The
auto-configured factory already carries the configured HTTP/async/metrics settings, so
factory.create(tenantUrls) inherits them — prefer that when you can.
When you build the factory programmatically (no starter), async, retry, and metrics are set
on a NotificationsConfig (builder), not properties:
// Shared once, reused across tenants.
Executor pool = Executors.newFixedThreadPool(4); // async delivery (caller-owned)
var config = NotificationsConfig.builder()
.ignoreChanges(List.of("*:PENDING", "*:RUNNING"))
.includeLog(false)
.http(HttpClientConfig.of(Duration.ofSeconds(5), Duration.ofSeconds(10), // connect, read
3, Duration.ofMillis(500))) // maxAttempts, backoff
.executor(pool)
.metrics(NotificationMetrics.NOOP) // or a Micrometer-backed impl
.build();
var factory = new NotificationsFactory<>(adapter, config);
// per tenant, at request time:
factory.create(tenant.channelUrls()).send(event, tenant.tags());
-
HttpClientConfig.of(connect, read, maxAttempts, retryBackoff)is the programmatic equivalent ofnotify4j.http.— *retry lives in core.maxAttempts=1disables it. -
.executor(pool)is the equivalent ofnotify4j.async.; omit it and the facade is synchronous — a slow channel blocks the caller. The executor is *caller-owned: reuse one pool across tenants and shut it down on app stop (Notifications.close()does not). -
Implement
NotificationMetrics(or reuse the starter’s Micrometer pattern) and set.metrics(…); the starter only meters its facade, not a hand-built one. -
NotificationsConfig.defaults()(ornew NotificationsFactory<>(adapter)) silently gives 10s timeouts, single attempt, synchronous, and no metrics — fine for a quick start, but call it out so multi-tenant apps don’t ship that unintentionally.
8. Channel discovery (catalog)
To build a "pick a channel → fill the fields → save" UI without hardcoding the scheme list
or URL assembly, query the read-only ChannelCatalog (since 1.1.0):
ChannelCatalog catalog = ChannelCatalog.standard();
// render a picker + form, data-driven (every channel notify4j adds shows up automatically)
for (ChannelDescriptor d : catalog.catalog()) {
for (ChannelField f : d.fields()) {
render(f.key(), f.type(), f.required(), f.secret()); // mask secret fields
}
}
// notify4j owns URL assembly (separators, default hosts, +http, ?tags) — never concatenate
var errors = catalog.validate("twilio", values); // field-keyed; empty == valid
String url = catalog.buildUrl("twilio", values, Set.of("failed"), false);
// edit screen: decompose a stored URL back into fields (secrets come back masked)
ParsedChannel parsed = catalog.parse(url); // values, tags, transport
String safe = catalog.redact(url); // scheme://host/… for display
buildUrl output is guaranteed to parse (a per-scheme round-trip test pins the catalog to the
URL parser). Secret fields are masked by parse (as ChannelCatalog.MASKED_SECRET); leave a
masked field untouched on save to keep the existing secret.