fix(intent): a process trigger records ProcessId before anything else can fail (#6815) - #6855
Merged
Merged
Conversation
… can fail (#6815) The stamped ProcessId is the guard that keeps a process starting at most once, and it is written AFTER the start, in a separate transaction. A failure in that gap left a running instance the record does not know about - invisible to the application, while its user tasks still reached an inbox - and the next qualifying event started another one. The gap is now as small as it can be, and no longer silent: - Everything that can happen before the start does. A minted businessKeyStrategy value is persisted BEFORE Process.start (the instance is started with that key), and every process variable - the __entityUrl/__entityId locators, the FK locators, __personalUser - rides the start payload instead of a post-start setVariable. They were all known up front, and the post-start calls were a live failure mode of their own: a process without a wait state runs to its end inside Process.start, and setVariable against a finished instance throws IllegalArgumentException - so such a trigger never recorded a ProcessId at all, and an onUpdate trigger then started an instance per event. - The write-back can no longer be REFUSED. It stays on the targeted updateProperty path, so the entity's own bookkeeping still runs (the history: trail entry attributed to SYSTEM, the stored label: Name), but the generated repository now runs its checks: gate only for a write that touches an AUTHORED column. A write carrying nothing but platform-owned columns - SYSTEM_PROPERTIES, today the trigger's ProcessId - cannot move the document into a gated status nor break what the gate checks, and by then its process instance is already running: an unbalanced or item-less document that records its process beats one that does not. - If the write still does not land - a swallowed start returning null, the row deleted meanwhile, or the write throwing - the trigger logs it and CANCELS the just-started instance (new Process.cancel SDK delegate), re-throwing the failure. An untracked live instance is the worst available outcome. Verified on the emission oracles plus the published app: IntentEngineIT asserts the ordering, the start payload and the cancel path; IntentEmissionCoverageIT asserts the conditional gate in a checks-bearing repository and, at runtime, that a created record carries a stamped ProcessId - which also keeps the history trail's SYSTEM attribution of that very write. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
delchev
force-pushed
the
fix/trigger-process-id-writeback
branch
from
August 19, 2026 13:31
04ea008 to
533cbbb
Compare
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 #6815.
The generated process trigger started the instance FIRST and recorded
ProcessIdSECOND, with independent commits between - and the guard against starting a second instance IS that stampedProcessId. A failure in the gap left a running instance the record does not know about, and the next qualifying event started another one.Two things came out of reading the path, beyond what the issue reported:
Process.start, and the trigger's very next calls wereProcess.setVariable(processId, …), which validates the id against the RUNTIME executions and throwsIllegalArgumentExceptionfor a finished instance. So such a trigger never recorded aProcessIdat all - the listener dispatcher swallowed it - and anonUpdatetrigger then started one instance per event.updatePropertyreally was refusable. The generated repository overrides it for achecks:-bearing entity and runsenforceCheckson the post-write row, which reads only the CURRENT status - so an item-less or unbalanced document in the gated status refused the write-back of a process it was already running.The shape now
Everything that can happen before the start does. A minted
businessKeyStrategyvalue is persisted BEFOREProcess.start(the instance is started with that key, so failing to store it must prevent the start, not follow it), and every process variable - the__entityUrl/__entityIdlocators, the FK locators,__personalUser- rides the start payload instead of a post-startsetVariable. All of them were known up front; a task created insidestartnow has its locators from its first moment, and a wait-state-less process no longer fails on a variable set against an instance that already finished.The write-back can no longer be refused. It stays on the targeted
updatePropertypath, so the entity's own bookkeeping still runs - thehistory:trail entry attributed toSYSTEM, the storedlabel:Name - but the generated repository now runs itschecks:gate only for a write that touches an authored column (SYSTEM_PROPERTIES/touchesAuthoredColumn, todayProcessId). Such a write cannot move the document into a gated status nor break what the gate checks, and by then its instance is running: an unbalanced document that records its process beats one that does not. Everything a person or a workflow authors keeps the gate, unchanged.(I first wrote this as an un-gated
JavaRepository.updateSystemPropertybypass. Rebasing onto master showed why that is wrong: thehistory:trail records exactly this write as the SYSTEM attribution a supervisory audit asks about, and a bypass silently dropped it -IntentEmissionCoverageITcaught it. The gate is the only thing that had to go, so the gate is the only thing that went.)And the residual failure is loud and clean. A swallowed start (
BpmProviderFlowable.startProcesslogs and returnsnull) is reported instead of written back as a null id; if the write returns 0 rows (the record was deleted in the meantime) or throws, the trigger logs it and cancels the just-started instance - a newProcess.cancelSDK delegate over the existingBpmFacade.deleteProcess- then re-throws. The cancel is deliberately tolerant: a wait-state-less instance has already finished and there is nothing left to cancel, and a cancellation problem must not replace the failure that led there.A hard crash between the two commits can still orphan an instance; that is inherent without a distributed transaction, and this module's consistency model is checks + compensation, not 2PC. The issue's third suggestion - a platform-level reconciliation query - is not expressible here: the instance-to-record mapping lives in the generated app, not in the platform, so a generic sweep has nothing to join on. It belongs in the intent DSL if we want it.
Verification
IntentEngineIT(emission): the ordering (the minted key is persisted beforeProcess.start), the whole variable map ridingJson.stringify(variables)with no post-startsetVariable, thenull-id guard, and the cancel-and-re-throw path.IntentEmissionCoverageIT(emission + published app): the conditional gate in the checks-bearing repository, and at runtime that a record created over REST comes back carrying a stampedProcessId- the same write whose SYSTEM attribution the history-trail assertion above it depends on. Green locally (127 s), together with the existing personal-assignee inbox assertion that only passes if__personalUserreally rides the start payload.javacagainst the real SDK andJavaRepository.formatter:validateclean.Docs: dirigible.io PR updates the
triggersection (it still described an@Listenerand said only "writes the instance id back").🤖 Generated with Claude Code