Skip to content

feat(intent): a notify body carries links, and the intent never spells a route (#6553) - #6744

Merged
delchev merged 1 commit into
masterfrom
feat/intent-notify-deep-links
Aug 15, 2026
Merged

feat(intent): a notify body carries links, and the intent never spells a route (#6553)#6744
delchev merged 1 commit into
masterfrom
feat/intent-notify-deep-links

Conversation

@delchev

@delchev delchev commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Closes #6553.

Problem

A notification that cannot be acted on is a notification that gets ignored — "you have an approval waiting" without a link is noise. {appUrl} (#6642) gave the notify block the application's origin, which left the author to type the rest:

body: "Review it here: {appUrl}/services/web/orders/gen/orders/index.html#/Order/{id}/edit"

That is the generated application's URL layout, hand-written into the one artifact that is forbidden to know it. It is correct until a template changes the layout, and silently wrong afterwards — and the issue explicitly asks for the opposite: "the concrete route should be assembled by the template layer (which knows the generated paths), keeping the intent layer path-agnostic."

Fix

Two reserved link tokens, usable in subject: / body: at every notify call site — the record link and the task link the issue names:

notify:
  to: Approver.email
  subject: "Approval needed: invoice {number}"
  body: "Open it here: {recordUrl}\nEverything waiting on you: {inboxUrl}"
Token Resolves to
{recordUrl} the record the message is about, opened in the generated application
{inboxUrl} the recipient's process Inbox
{appUrl} unchanged — the origin only, now the escape hatch for what the two above cannot express

The split is the whole design. NotificationSupport.Resolver resolves each token to a bare Java identifier and records the use. NotifySupport.deepLinkFields carries the only facts the intent layer owns — recordUrlEntity / recordUrlKeyProperty (model facts, not paths) — plus a uses* flag per link. The events template declares the local:

String recordUrl = Configurations.get("DIRIGIBLE_APP_BASE_URL", "")
        + "/services/web/orders/gen/orders/index.html#/Order/" + entity.Id + "/edit";

So the route is composed in the layer that knows it — the same reason Trigger.java.template, not the generator, assembles __entityUrl for the task form — the path-agnostic rule holds, and a change to the generated app's URL layout stays a template change alone.

Notes

  • All four notify call sites got it from the two shared seams (buildNotifications, buildSchedules, and notifyFields → transitions + sends), so no call site can be forgotten.
  • A link is declared only where the message names it — no dead local in generated code.
  • Inside a fan-out {recordUrl} links the ROW, like every other bare path in the block; {record.<field>} keeps reading the anchor for values.
  • The origin is still DIRIGIBLE_APP_BASE_URL, tenant-overridable, read per dispatch in the sending tenant's configuration scope (#6649).

Tests

  • NotificationSupportTest — both tokens resolve to the bare locals and are reported; an unreferenced link is not reported.
  • IntentEngineIT — the composed locals at the notification and schedule call sites, the unused link not declared, and recordUrl never falling through to a non-existent entity.RecordUrl.
  • IntentEmissionCoverageIT — the transition and the fan-out send, including that a fan-out links the ROW (#/BillRecipient/) and not the anchor. This gate is also the compile proof: an undeclared local fails the whole client-Java batch and takes every REST assertion with it.

Both ITs green locally (IntentEngineIT 46/46, IntentEmissionCoverageIT 1/1), plus the 543 unit tests of the two changed modules.

Docs

  • dirigible-io.github.ioPR #185 (mine to merge).
  • IntentFile/intent-specification + IntentFile/intentfile.github.io — PRs opened for the maintainer; the spec did not document the link placeholders at all, so both introduce the whole family.

🤖 Generated with Claude Code

…s a route (#6553)

A notification that cannot be acted on gets ignored, and until now "you have an
approval waiting" could only carry a link by hand-typing one:

    body: "Review it here: {appUrl}/services/web/orders/gen/orders/index.html#/Order/{id}/edit"

That is the generated application's URL layout, typed into the one artifact
that is forbidden to know it - correct until a template changes the layout, and
silently wrong afterwards. {appUrl} (#6642) supplies only the origin, so it was
the author's job to append the rest.

Two reserved tokens finish the job the issue asked for - a link to the record
or to the task:

    body: "Approve it here: {recordUrl}\nEverything waiting on you: {inboxUrl}"

The split is the design. NotificationSupport resolves each token to a BARE JAVA
IDENTIFIER and records the use; NotifySupport.deepLinkFields carries the only
facts the intent layer owns - the entity and its key property, model facts and
not paths - plus a uses* flag per link; and the events TEMPLATE declares the
local, composing <appUrl>/services/web/<project>/gen/<genFolder>/index.html
#/<Entity>/<id>/edit from parameters it already has. So the route lives in the
layer that knows it (the same reason Trigger.java.template, not the generator,
assembles __entityUrl), the path-agnostic rule holds, and a route change stays a
template change.

All four notify call sites picked it up from the two shared seams
(buildNotifications, buildSchedules, and notifyFields -> transitions + sends),
so none can be forgotten. A link is declared only where the message names it -
no dead local in generated code - and inside a fan-out {recordUrl} links the
ROW, like every other bare path, while {record.<field>} keeps reading the anchor.

Covered by NotificationSupportTest (both tokens, and the unused one not
reported), IntentEngineIT (the composed locals at the notification + schedule
sites, the unused link NOT declared, and recordUrl never falling through to a
non-existent entity property) and IntentEmissionCoverageIT - where it is also
the compile proof, since an undeclared local fails the whole client-Java batch
and every REST assertion in that gate.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@delchev
delchev merged commit ac804cb into master Aug 15, 2026
10 checks passed
@delchev
delchev deleted the feat/intent-notify-deep-links branch August 15, 2026 18:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

intent: notification deep links - app base-URL token in notify bodies

1 participant