feat(intent): send a document by e-mail - the notify block + attach: print (#6356) - #6435
Merged
Conversation
…print (#6356) An intent could render any document to PDF (print templates) and could send plain-text mail from a schedule, but not do the single most common outbound action a business document has: send the document itself - the invoice to its customer, the payslip to its employee, a reminder carrying the invoice. `to` / `subject` / `body` (+ `channel`) becomes ONE reusable notify block, authored at every place an intent can act on a record: notifications[] on the entity's create / update / delete schedules[].notify per row matched by a cron query transitions[].notify after a guarded status flip commits serviceTask args.notify when a process flow reaches that step `attach: print` renders the record's own .print template server-side - the generated <Entity>PrintFeeder assembles the { document, items } payload and sdk.print.Print renders it, the same two steps the snapshot delegate takes - and attaches the PDF, named after the document's `number:` field. Failure semantics differ per call site, on purpose: a recipient that resolves to nothing is a logged no-op (a record with nobody to mail must not stall a flow); a transition's mail can NEVER fail its transition (the flip is the endpoint's contract and has already committed); a sending serviceTask DOES fail so the process engine retries, because the message is that step's work. Only a document (header + line-items child) can be attached - the parser rejects `attach: print` on anything else rather than let a mail go out claiming a document it cannot render. A sending serviceTask stands alone: notify cannot share a step with setField / setRelationField / call / delegate. MailClient now accepts a part's `data` as a raw byte[] in addition to the historical JSON int-array string, so a rendered PDF does not have to travel through a JSON array. Emission + runtime coverage in IntentEmissionCoverageIT: the PDF attachment part, the feeder-backed Print.render, the one-hop FK recipient load, the BPMN binding of the generated sender, no scaffolded custom stub - and at runtime a notify-carrying transition still returns 200 with the status written when the mail cannot be delivered, plus the no-recipient no-op on a sending step. Closes #6356 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…keyProperty The smoke gate caught it: IntentEngineIT asserts that an opted-out trigger leaves no trigger entry in the .glue, and it used "keyProperty" as the proxy for "this is a trigger entry". Adding keyProperty to the notifications / schedules entries (the PK an attach: print feeds the print feeder with) made that proxy match non-trigger glue, so the test failed on a true statement about the new key rather than about triggers. The attachment's PK reference is now attachKeyProperty on notifications, schedules and transitions, grouped with the other attach* keys. `sends` keeps keyProperty, where it genuinely IS the step's process variable - the same meaning setters and writers give it. The IT's proxy now keys on "businessKeyProperty", which only a trigger emits, so the next glue block that legitimately needs a PK reference cannot break it; the comment records why. Generated code is unchanged (same value, different glue key). Verified: engine-intent unit tests green, IntentEngineIT#settings_overrides_skip_generation_ and_are_preserved green, IntentEmissionCoverageIT green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…of printing
Two things, both found in review of the send-document glue.
1. FAN-OUT. A notify block resolved exactly one recipient, so the two cases the
suite actually needs were impossible to express: a payroll run that mails
every payslip to its own employee, and a request for quotation that goes out
to each invited supplier. `notify.forEach: <Entity>` now sends ONE message per
row of a related entity - the rows whose to-one FK points back at the record -
and every path resolves against the ROW: the recipient, the {placeholders},
and the attached document (attach: print renders the ROW's own print).
The row entity must have exactly ONE to-one relation back to the record; zero
means the rows are unrelated, two or more make the intended set ambiguous, and
both are parse-time errors rather than a quietly wrong set of recipients.
Per-row fail-soft, always: a row with no address is skipped, a failed send is
logged, and the step/transition completes with a summary. Failing the task
would have the engine retry the WHOLE fan-out and mail everyone who already
received their message a second time - a partial send is not idempotent.
2. LOGGING. The send templates shipped System.err.println / System.out.println.
Generated code now takes the client SDK logger (sdk.log.Logging, nested under
the platform's `app.` root) with SLF4J-style placeholders and the throwable
passed last - so the output has a level, a logger name and a home in the Logs
view, and can be turned down in production. The `custom/` stub
ServiceTaskHandlerGenerator scaffolds does the same, because a generated class
is read as house style by every application developer.
The rule is now written down where it cannot leak again: "Never print to
stdout or stderr - log" in the root CLAUDE.md conventions (with the single
documented exception - ConsoleWebsocketHandler.distribute is called BY the
appender, so logging there recurses), and the generated-code half in
engine-intent/CLAUDE.md.
Verified: engine-intent 243 tests green, incl. four new fan-out cases (the
payroll shape end to end, the empty-keys contract without a fan-out, and both
parser rejections).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This was referenced Jul 28, 2026
The custom/ service-task stub stopped printing and started logging through the SDK logger in 69494fc, but IntentEngineIT still asserted the old printed text ("OrderApproval: notifyCustomer service task executed."), so smoke-tests went red on the one line the commit had made obsolete. The engine-intent unit tests moved with the template; this integration assertion did not. It now pins what the stub actually emits - the named logger and the parameterised default message, both resolved for the OrderApproval/notifyCustomer step - plus a System.out/System.err guard, so the next time a generated class reaches for a print the test says so instead of a string comparison quietly aging out. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
delchev
added a commit
that referenced
this pull request
Jul 28, 2026
One conflict, in the module guide's `postings:` bullet, which both sides rewrote: this branch documented the new onCreate trigger, master (#6435) reworded the same sentence's spike-findings reference. Resolution keeps both meanings - the onCreate documentation, and the NEUTRAL wording for the reference: master's rewording named a downstream product, which does not belong in this repository's content. Everything else auto-merged. Verified rather than assumed, since both sides had edited IntentParser, GlueIntentGenerator, generateUtils.js and IntentEmissionCoverageIT: the engine-intent suite is green at 252 tests, including BOTH sides' new cases (GluePostingsTest from this branch, GlueSendDocumentTest from master), and tests-integrations compiles. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
delchev
added a commit
that referenced
this pull request
Jul 29, 2026
…ch) (#6445) The notify fan-out shipped with #6435 - NotificationIntent.forEach, NotifySupport .fanOut, the glue keys, Send.java.template's per-row loop and thirteen unit cases are all on master - but the authoring guide never got its section: no "fan-out", no forEach row in the keyword table. The only forEach the guide describes is the unrelated generates/children collection (`forEach: { entity: … }`), so an assistant reading the guide cannot author the keyword the engine accepts, and would plausibly try the map shape, which a notify block does not take. The text was written when the feature was built and never left a working tree. It documents the shape (`forEach: <Entity>`, every path then resolving against the ROW - recipient, {placeholders} and the row's own attach: print), the exactly-one-back- relation rule, and why a fan-out is fail-soft per row rather than failing the step: a retry would re-mail everyone already served, so a partial send cannot be idempotent. Each claim was checked against master rather than trusted: the plain-string forEach in NotificationIntent, the exactly-ONE rule in IntentParser.validateNotifyBlock (unknown entity and zero-or-ambiguous back-reference both reported), the sent / no-recipient / failed counters and the summary log in Send.java.template, and attach: print re-pointing at the row entity. Docs only - no code, no behaviour change. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Closes #6356. An intent could already render any document to PDF and send plain-text mail from a schedule, but not do the single most common outbound action a business document has: send the document itself - the invoice to its customer, the payslip to its employee, an escalating reminder that carries the invoice.
to/subject/body(+channel) becomes one reusable notify block, authored at every place an intent can act on a record:notifications[]<Name>Notification.javaschedules[].notify<Name>Job.javatransitions[].notify<Name>Transition.javaserviceTask'sargs.notify<Process><Step>Send.java(newsendsglue)attach: printreuses the generated<Entity>PrintFeeder+sdk.print.Print- the two steps the snapshot delegate already takes - so no module ever needs a hand-written listener around the print engine. The attachment is named after the document'snumber:field (INV0000042.pdf), else<Entity> <id>.pdf.Failure semantics, per call site (deliberate)
transitions[].notifycan never fail its transition: the flip is the endpoint's contract and has already committed, so SMTP cannot turn a successful void into an error.serviceTaskdoes fail on a delivery error, so the process engine retries - the message is that step's whole work.Guardrails
attach: printon anything else rather than mail a note claiming a document it cannot render. The check mirrors the print generator's own document-master resolution, so the parser can never allow what generation would silently drop.serviceTaskstands alone:notifycannot share a step withsetField/setRelationField/call/delegate.ServiceTaskHandlerGeneratorno longer scaffolds acustom/<Step>.javastub for a notify step (it would never be invoked).Platform side
MailClientaccepts a part'sdataas a rawbyte[]in addition to the historical JSON int-array string, so a rendered PDF does not have to travel through a JSON array of numbers. Backward compatible; both shapes are unit-tested.Test
GlueSendDocumentTest- the pre-rendered glue at all four call sites, the empty-key contract for agenerateschedule, and both parser rejections (non-document attach, notify combined with setField).MailClientAttachmentTest- abyte[]and a JSON-array attachment reach the MIME part byte-identically.IntentEmissionCoverageIT(extended fixture) - emission: the PDF attachment part,Print.render("Bill","en", new BillPrintFeeder().feed(entity.Id)), the one-hop FK recipient load, the fail-softcatch, the BPMN binding ofBillFlowMailBillSend, and no scaffoldedcustom/MailBill.java. Runtime: a notify-carrying transition returns 200 with the status written when the mail cannot be delivered, and a sending step logs its no-recipient no-op.engine-intentsuite +api-mailgreen; formatter clean.Docs
intent-assistant-guide.mdgains a send a document by e-mail section and reference-table rows. The vendor-neutral spec (intentfile.org) and the platform docs (dirigible.io) are updated in their own PRs, per the documentation-sync contract inengine-intent/CLAUDE.md.🤖 Generated with Claude Code