docs: when a cancelation arrives, and what the election carries
Workflow tasks: the @TaskEvent paragraph said cancelation was a property of the BPMS
and left it there. It now says when the event arrives on a remote BPMS, which is at the
end of the workflow and at the next job of the same workflow, what that does not catch,
and how to switch the check off. The schema paragraph names the second index the
delivery table needs.
BPMS migration: the election may carry the BPMS' own id of the workflow, what an
adapter may do with it and what it may never do with it.
Extensions: a section of its own about which BPMS holds a workflow, the two calls, and
what an extension may do with the workflow id.
Both platform pages: the delivery table gets the indexes its reads need, not only the
one its retention deletes by.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RYPiyZ32jQQhqYKx44cw23
docs(outbox): the youngest call replaces the one still waiting
An extension whose call carries a payload carries a state, and the entry which
waited holds the older one. It may now say replacingWhatIsStillWaiting() and go
to scheduleReplacingWhatIsStillWaiting, and the waiting entry gets the younger
call instead of dropping it.
The extensions page gets the section with the example, what happens to the
payload of the entry which was replaced, and the one entry which is never
replaced. Both platform pages get it in the list of what the outbox guarantees,
including what a store of your own has to implement.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RYPiyZ32jQQhqYKx44cw23
docs(extensions): work an extension plans for after the commit
The outbox is open to extensions, and a call of theirs may now carry the state
they saw at their sync point. The new section says what that gets an extension
author, what it costs and where the bytes lie; the two platform pages name the
third table in the list of changelog properties.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 317c593d1dc4f979b02bced2fe22416d596a1c0b)
docs: a rejected dispatch is planned again instead of waited for
The gruelbox store used to wait out the window of a workflow its BPMS had not
made searchable yet, on its dispatching thread. It cannot: gruelbox dispatches
inside a transaction of its own, and a rejected attempt leaves that transaction
unable to commit. The attempt now ends and the entry is planned again with the
window the adapter named, so the page says where that window is written and what
the store adds to it.
The migration page promised one to three seconds for a Camunda 8 workflow which
was not searchable yet. What decides is the adapter's window, so the page says
that and where to lower it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019HSy3LqLy3SDHdP4ojgZb2
docs(schema): the indexes are part of the tables
An application which applies the schema artifact itself has to apply them too, so
both platform pages say which indexes there are, what the question behind them is
and what it costs without them: 11 to 18 ms of sequential scan against 0.05 ms on
PostgreSQL 16 with 200000 rows.
They also say what happens where VanillaBP created the tables in an earlier
version: the boot names the missing index and the statement which adds it rather
than creating it itself, because that takes a lock for as long as the table is
large.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Re2yjHbaFRvnHyW61E3sao
docs(outbox): the poll interval is a cap, not a rhythm
The poller asks its store when the earliest entry it owes something to is due
and sleeps until then, so an entry goes out at its due time whatever the
interval says. Both platform pages said the opposite, in the property comment
and next to the statement which reopens a blocked entry.
What the interval still bounds is work a node wrote down before it went away,
which is where the cluster page now says why no node wakes another and why a
database notification was not the answer.
Observability gains the other half: a sleeping poller takes the heartbeat away,
so the page says which number tells a quiet system from a stalled one now, and
where to look in a thread dump.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Re2yjHbaFRvnHyW61E3sao
docs(outbox): the store waits for the deployment on every store
Spring Boot answers requests before the models reached the BPMS, and until now the
gruelbox store carried a start arriving in that window to a BPMS which does not hold
the process. It waits for the first poll now, like the stores VanillaBP owns, so the
page says what a reader has to expect at an application start: one poll interval at
worst, and no start blocked for having been early.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Re2yjHbaFRvnHyW61E3sao
docs(outbox): a blocked gruelbox entry keeps its key
The per-store table promises that a key deduplicates only what is still waiting.
Gruelbox holds it until the row is gone, blocked entries included, so a repetition of
the operation is refused while the row waits for repair. That was reachable after
block-after-attempts before and is reachable on the first attempt now that a failure
the adapter calls permanent blocks right away, which is reason enough to write it down
next to the other two things a JPA application inherits.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Re2yjHbaFRvnHyW61E3sao
docs(outbox): a start which never happens is blocked and visible
Observability gains vanillabp.outbox.blocked, the counter of entries a store gave up
on. It needs a row of its own because vanillabp.outbox.pending falls at that very
moment: a blocked entry stops waiting, so the gauge an operator watches moves as if
the backlog had drained.
Starting workflows now says that on a remote BPMS the start finishes after the
caller's commit, that the second half can fail with nobody in the application
listening, and where to look when it does. The retry curve stays on the platform
pages, which the section links to.
Both platform pages point the operations duty at the new counter and say which
values the error of a blocked entry names.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Re2yjHbaFRvnHyW61E3sao
docs: what gruelbox does with a workflow which is not searchable yet
The page said that this store knows one fixed distance and left it there, which was true
for a failed dispatch and misleading for the one case an application notices most: a
message correlated right after its workflow was started. That entry carries the window
its BPMS needs, and gruelbox now waits it out instead of coming back after
attempt-frequency.
The paragraph says both halves, because the second one is the price: the thread waiting
is the one dispatching this store, so everything else waiting in it waits along, and an
application which cannot afford that runs the JDBC store.
docs: the retry distance grows, and a blocked entry no longer blocks itself
Both platform pages described the old envelope: thirty seconds ten times, about
five minutes, and a BPMS away for longer turned every pending operation into a row
somebody opened by hand. The distance grows now, attempt-frequency doubling up to
max-attempt-frequency, and the pages say what the two ends answer - the cap decides
how quickly a returned BPMS is noticed, the attempt budget decides which outage the
store survives on its own.
The dead end goes with it: blocking releases the deduplication key on the stores
VanillaBP owns, so a repaired entry has to be expected next to a second row for the
same operation. Where gruelbox cannot follow, the Spring Boot page says so in the
place its other differences already stand: one fixed distance instead of the growing
one, and a request id held until the row is removed.
Story 195, carrying the wiki text of story 186.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
docs: the page is called integration SPI
The artifact is io.vanillabp:vanillabp-integration-spi, so the wiki calls the
thing what its coordinate calls it instead of inventing a second name. Page
renamed, all links and the glossary entry follow.
The adapter SPI is gone from the wiki: it is nothing a reader of these pages
implements. Where the start instant explains its own name, the reason is now
VanillaBP's rather than an SPI the page never introduces.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XjxJNwTRcKzTmXCwLU7Wi4
docs: a page for the business SPI, testing parked
The wiki mentioned the business SPI and the two support modules without ever
saying what they are or why they exist. Business-SPI.md answers both: the two
APIs and their directions, why these interfaces are released with the platform
instead of with the SPI for Java, the five contracts an application may
implement, and how the artifact reaches a build. Linked from Home, the
migration page, both platform pages and the aggregates page.
Migrating from version 1 no longer mentions states version 2 passed through on
its way, and no longer names the Process-Engine-API adapter: version 1 had no
support for that BPMS, so nobody upgrades from it.
Testing.md moves to parked/Testing.md.txt. GitHub creates a page for every file
with a markup extension, so the .txt keeps it out of the wiki's page list while
the text stays in the history until the reworked testing strategy arrives with
the blueprints.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XjxJNwTRcKzTmXCwLU7Wi4
docs: how long the outbox tries, and the way back
An operator was told that blocked entries need manual attention and neither how long the
framework tries before an entry becomes one nor what that attention is. Both platform pages now
name the envelope as the product of attempt-frequency and block-after-attempts, say that a
blocked entry stays in the store on purpose and that retention never removes it, and carry the
way back: the statement for VanillaBP's own stores, gruelbox' unblock for the JPA default on
Spring Boot, and the at-least-once consequence of reopening an entry next to it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XjxJNwTRcKzTmXCwLU7Wi4
docs: one blank line between paragraphs, on the two platform pages
Removing the note markers left a double blank line behind. One appearance for all
pages means one blank line here too.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
docs: every page opens with what to do and carries a table of contents
A reader who has never used VanillaBP met the reasoning for a rule before the rule
itself, and had no way of telling how far to read before writing a first workflow.
The pages now start with the sentence somebody can act on and keep the background
where it belongs, further down. Nothing was deleted; what explains a rule moved
behind it.
Every page gained a numbered table of contents in the shape Home and the Spring Boot
page already used, so a reader sees the sections and what each of them answers before
scrolling. Where a first heading only repeated the title GitHub shows above the page,
it is gone and the page starts with its first sentence.
Home is now ordered the way a newcomer needs it, and what VanillaBP is sized for
became a page of its own: it answers a question somebody asks before they start, not
one they trip over in the middle of the introduction. The architecture picture from
the repository's diagrams folder is embedded there, linking to its Excalidraw source.
The horizontal rules of the workflow-module pages are gone, the heading is the
separator everywhere else. Hints are written as sentences rather than marked with a
label, headings sit at one depth per page, and the paragraphs written by hand over
the last months were brought to the tone of the rest without changing a single
statement they make.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
docs: completing a task reads the record instead of asking a BPMS
The delivery record of a task names the BPMS which handed that task out, so
completeTask and cancelTask are routed from it and a second call is the warned
no-op without a round trip. Workflow-tasks gets the section which explains it and
what happens where no record answers; BPMS-migration says that this is the one
operation for which a shared election cache buys nothing, and the two platform
pages name the column and the index the store needs for it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XjxJNwTRcKzTmXCwLU7Wi4
docs: a dependency example carries the version you can paste
The rule is about prose: a version named in a sentence reads 2.0, the release line without a
patch digit. A Maven or Gradle coordinate is not prose - it is copied into a build, where 2.0
resolves to nothing - so the examples carry 2.0.0 again, and the Camunda 8 ones 2.0.0-8.9,
with the suffix naming the cluster minor the artifact was built against.
The sentence which told the reader to fill in a patch himself is gone with it; where it is
useful to know which patches exist, the pages link Maven Central instead.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvgyhjwTEh834mwbptZZxh
docs: a rule says where its decision is written down
Five decision logs hold 61 entries, the wikis linked none of them, and 22 of the 42
objections an outside review raised were already answered in one. A reader who wondered why
a rule is what it is had no way there short of reading source repositories.
So a rule carries a (why) link now, pointing at the numbered entry which decided it. Home
says once what the links are and where the logs live, and the form is the same on every page
of every wiki: an inline link at the end of the sentence stating the rule. Numbers are handed
out once and never reused, so the links keep resolving.
The rules the review named are linked first - the outbox on Camunda 7, probing per operation,
the two retentions, sharing aggregate values, the Camunda 8 correlation key - and the
remaining entries follow wherever a page states their rule. BPMS-adapters also says in one
sentence what the module migration-adapter is, which it linked without explaining.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvgyhjwTEh834mwbptZZxh
docs: why FULL is the default, what the stores differ in, and what the trigger time is
Three gaps the review found on these pages.
Workflow-aggregates stated the default and not its reason, so a reader minimising with
@NoSyncWithBPMS could not tell which side to minimise. The reasons are portability and an
upgrade from version 1 which must not touch code, and neither of them says that sharing
everything is right: what to aim for is as little as possible and as much as needed, which
means the class annotated @NoSyncWithBPMS and @SyncWithBPMS on the attributes the process
decides by plus the ones an operator has to see in Operate or Cockpit. The page also names
what ignoring it costs, payload per command on Camunda 8 and a serialization format on
Camunda 7.
The same page says when the values arrive, which differs: inside the engine transaction on
Camunda 7, with the completing command on the other two, so only there can a failure leave
the aggregate ahead of the BPMS.
The outbox guarantees were stated for every store, and "DONE, not deleted" does not hold for
the gruelbox store of a JPA application: it deletes a dispatched row to free the key, and it
counts an attempt only when one failed, so a hard crash re-dispatches without the probe which
mitigates a duplicate start. The Spring Boot page carries a table per store now, says what
follows for a JPA application, and says that a future version replaces that default with the
JDBC store Quarkus uses. The Quarkus page points there for whoever runs both.
Starting-workflows called the aggregate's id the timer's trigger time. No engine reports the
scheduled time, so what an embedded engine answers is the moment the instance was created,
which costs nothing because both are written in one transaction.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvgyhjwTEh834mwbptZZxh
docs: the pages follow the decisions taken since 2026-08-11
Camunda 7 progresses a workflow after the caller's commit, through the outbox, since
story 63. Three pages still described the model before it: correlating a message,
broadcasting a signal and pushing a changed aggregate all said an embedded BPMS does the
work inside the caller's transaction. They now say what every adapter does, with the
reason in one sentence - a command which loses a concurrency conflict leaves the caller's
transaction rollback-only, so it cannot be repeated there. The ordering paragraph of
Workflow-aggregates loses its embedded exception for the same reason: an embedded BPMS is
dispatched by the same outbox and promises no order either.
A workflow module's configuration file carries defaults again (2026-08-21), so the Spring
Boot and Quarkus pages stop promising it higher priority than the application's own files.
Workflow-modules-in-Spring-Boot no longer says VanillaBP scans the classpath for workflow
services: they are found among the bean definitions since 2026-08-26, which changes what a
module nobody scanned looks like at startup.
The migration guide's property table said retry-backoff is "not yet in version 2". It is
back on the Camunda 8 adapter, PT10S by default and resolvable down to a single task, so
the row now points there instead of asking readers to report it.
Versions read 2.0 everywhere, without a patch digit and without -SNAPSHOT, and the pages
which introduce the BOM say once what that means for a build.
Two anchors were dead, workflow-visibility-timeout on the Camunda 8 Configuration page and
a section about the aggregate's ID which does not exist on Workflow-aggregates.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvgyhjwTEh834mwbptZZxh
docs(delivery): the retention of a delivery record is its own property
vanillabp.delivery.retention decides how long a record is kept, and it follows
vanillabp.outbox.retention only where it is not set. The pages now say which of the two
numbers is about correctness and which one is about support, that the new one is read
globally rather than per workflow module, and what makes a period long enough - including
the part nobody can compute for the reader, how long the application is stopped.
docs(correlation): what an idempotency key deduplicates, and what a loop has to do
The key deduplicates the operations still waiting for their dispatch, so the second round
of a loop correlating the same message with the same correlation id reaches the BPMS
instead of being swallowed for the retention period. What still collides is two
correlations planned in one unit of work, and a multi-instance call activity is where the
sameness of the aggregate is not the application's choice - so the page says to vary the
correlation id per round and per element, and that VanillaBP warns about every discard.
Camunda 8 gets a section of its own: the cluster deduplicates by message id for the message
time-to-live, one hour by default, which is the longer of the two nets and shortened by no
VanillaBP setting.
The outbox sections of both platform pages say what the retention still buys after this,
and `Workflow-tasks` says that the same property decides correctness on the inbound side.
Story 128.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvgyhjwTEh834mwbptZZxh
docs: VanillaBP notices the leftovers of a renamed adapter id
Story 120 makes the rule of story 119 checkable: at startup VanillaBP asks its own
stores which adapter ids they still hold waiting outbox entries or records of open
tasks for, and names one which is not configured any more, with both readings and
both remedies.
BPMS-migration says what the message looks like, why it warns instead of failing the
boot, and how 'vanillabp.retired-adapters' quits the leftovers of a finished
migration. It also names the two limits: a store which cannot answer keeps the check
silent (the gruelbox-based outbox is one, its entries are a serialized invocation),
and a rename while nothing is left is undetectable and costs nothing. Both platform
pages list the new property.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K7HLKaJvVpdCiSYxhwHJTc
docs: an adapter id is a name to choose once and never rename
Story 119 showed how easily an adapter id is taken for a label. It is not: VanillaBP
persists it to know what belongs to which BPMS, so a rename is a new adapter next to
workflows nothing serves any more.
BPMS-migration gets the authoritative section: why a speaking id is worth choosing up
front (which cluster, which database, SaaS next to on-premises, and a migration
between two Camunda 8 configurations needs two names anyway), a caution that an id
must never be renamed while workflows run, and a table of what a rename costs - the
open START entries of the outbox, the delivery key of every processed delivery, the
election cache and the process definition ids an application already holds.
Both platform pages say in their convention list that "the type defaults to its ID"
exists for applications upgrading from version 1 rather than as a recommendation, and
point at that section. Migrating-from-version-1 says the same where an upgrader reads
it: keep the id while one BPMS is all you run, and know that the second one cannot be
had by renaming the first.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K7HLKaJvVpdCiSYxhwHJTc
Merge remote-tracking branch 'origin/master' into feature/finalize
# Conflicts:
# Quarkus-integration.md
# Spring-Boot-integration.md
docs: what keeps the record of a task nobody has completed
The record answering the redeliveries of an open task used to expire with
vanillabp.outbox.retention while the task was still open, and the page said so as a
boundary to plan around. It is not one any more: the retention counts from the last
redelivery the record answered, the moment the handler ran is a second timestamp
which never moves, and the age of an open task keeps being measured from that one.
Also said on both platform pages: a store of your own decides how long its records
live, and stillOpen is what VanillaBP calls on every redelivery it answers from a
record.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
docs: what a Flyway instance of its own needs, per platform
Story 96, from what the blueprint persistence-flyway measured on both platforms.
Story 75 recommended a Flyway instance with a history table of its own for
VanillaBP's SQL and did not say what that costs.
On Spring Boot it needs baselineOnMigrate, because every instance but the first
finds a schema full of somebody else's tables and refuses to touch it, and
baselineVersion("0") with it, because the default baseline is version 1 and would
mark every migration numbered 1.0.0 as applied while reporting success. Named the
FlywayMigrationInitializer as well: the entity manager factory waits for beans of
that type, not for Flyway beans.
On Quarkus the recommendation does not hold at all. The extension applies one
configuration per datasource, a named one without its datasource is skipped without
a word, and a migration run of the application's own comes after Hibernate has
already validated the schema. The page now describes the shape which does work
there: one timeline for every owner, with the version ranges kept apart, and
VanillaBP numbering its own in 2.x.
Both pages point at the blueprints persistence-liquibase and persistence-flyway,
where both shapes have a test behind them.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
docs: the outbox table on Spring Boot belongs to gruelbox
Story 95. Handing the schema over with 'vanillabp.outbox.create-schema: false'
switches gruelbox's migrator off together with VanillaBP's table creation, and a
custom table name does the same silently. The page said so in three lines and left
the reader to find the statements; it now says that the table is checked at startup
like VanillaBP's own two, and where the statements come from:
DefaultPersistor.writeSchema(Writer) emits gruelbox's migrations for the configured
dialect, so nothing has to be copied out of its source.
Also noted what that output does and does not contain: TXNO_SEQUENCE belongs to it,
TXNO_VERSION does not, being the bookkeeping of the migrator just switched off.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
docs: how long a delivery is remembered
Story 76. The deduplication window was the retention period alone. It becomes:
until the workflow ends, or until the retention passed, whichever comes first -
and with the new option switched off the old sentence still holds.
Workflow-tasks gets the section, next to what a repeated delivery does; both
platform pages get the property and the note that a store of its own which
cannot delete is named at startup.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jss55UjjvxYcLQPVFUFe25