NullPointerException in Connector call from BPM

Hello,

I’m trying to develop a business process to manage Purchase Quotations / Purchase Orders. The first step would be to automatically make the purchase quotation requested. My business process is defined as follows:

Request:



Connector:

BPM:



I create a Request Qotation, fill some data then press Save. I get a NullPointerException with the following stack trace:

2025-10-13 09:01:09.570 DEBUG 10842 — [nio-8000-exec-8] c.a.s.b.s.e.WkfInstanceServiceImpl : Start instance for the model: com.axelor.apps.purchase.db.PurchaseOrder, id: 318
2025-10-13 09:01:09.572 DEBUG 10842 — [nio-8000-exec-8] c.a.s.bpm.service.WkfCommonServiceImpl : Process variables: {purchaseOrderId=Value ‹ 318 › of type ‹ PrimitiveValueType[long] ›, isTransient=false, purchaseOrder=ObjectValue [value=com.axelor.utils.context.FullContext@4a8d7c9, isDeserialized=true, serializationDataFormat=application/json, objectTypeName=null, serializedValue=null, isTransient=true]}
2025-10-13 09:01:09.577 DEBUG 10842 — [nio-8000-exec-8] c.a.s.bpm.listener.WkfExecutionListener : Process called with related wkfInstance: null
2025-10-13 09:01:09.591 DEBUG 10842 — [nio-8000-exec-8] c.a.s.bpm.listener.WkfExecutionListener : Task config searched with taskId: Activity_1858ivt, processInstanceId: 327598, found:WkfTaskConfig{id=4, name=Activity_1858ivt, expression=$ctx.find(‹ PurchaseOrder ›, purchaseOrderId)?.createdOn != null, createTask=false, notificationEmail=false}
2025-10-13 09:01:09.618 DEBUG 10842 — [nio-8000-exec-8] c.a.s.b.s.e.WkfInstanceServiceImpl : Model process instanceId added: 327598
2025-10-13 09:01:09.639 DEBUG 10842 — [nio-8000-exec-8] c.a.s.b.s.execution.WkfTaskServiceImpl : Variable map used: {purchaseOrder=com.axelor.utils.context.FullContext@12425e33}
2025-10-13 09:01:09.642 DEBUG 10842 — [nio-8000-exec-8] c.a.s.bpm.service.WkfCommonServiceImpl : Process variables: {purchaseOrderId=Value ‹ 318 › of type ‹ PrimitiveValueType[long] ›, isTransient=false, purchaseOrder=ObjectValue [value=com.axelor.utils.context.FullContext@12425e33, isDeserialized=true, serializationDataFormat=application/json, objectTypeName=null, serializedValue=null, isTransient=true]}
2025-10-13 09:01:09.644 DEBUG 10842 — [nio-8000-exec-8] c.a.s.bpm.service.WkfCommonServiceImpl : Process variables: {}
2025-10-13 09:01:09.724 DEBUG 10842 — [nio-8000-exec-8] c.a.s.bpm.service.WkfCommonServiceImpl : Eval expr: $ctx.find(‹ PurchaseOrder ›, purchaseOrderId)?.createdOn != null, result: true
2025-10-13 09:01:09.724 DEBUG 10842 — [nio-8000-exec-8] c.a.s.b.s.execution.WkfTaskServiceImpl : Valid expr: $ctx.find(‹ PurchaseOrder ›, purchaseOrderId)?.createdOn != null
2025-10-13 09:01:09.745 DEBUG 10842 — [nio-8000-exec-8] c.a.s.bpm.listener.WkfExecutionListener : Task config searched with taskId: Activity_1858ivt, processInstanceId: 327598, found:WkfTaskConfig{id=4, name=Activity_1858ivt, expression=$ctx.find(‹ PurchaseOrder ›, purchaseOrderId)?.createdOn != null, createTask=false, notificationEmail=false}
2025-10-13 09:01:10.005 DEBUG 10842 — [nio-8000-exec-8] c.a.s.service.ws.WsConnectoServiceImpl : URL: http://ubuntu.mshome.net:8000/axelor-erp-latest/ws/action
2025-10-13 09:01:10.021 DEBUG 10842 — [nio-8000-exec-1] c.a.meta.schema.actions.ActionGroup : action: action-purchase-order-method-requested
2025-10-13 09:01:10.046 ERROR 10842 — [nio-8000-exec-1] c.a.a.b.s.exception.TraceBackService : java.lang.NullPointerException
at com.axelor.apps.purchase.service.PurchaseOrderServiceImpl.requestPurchaseOrder(PurchaseOrderServiceImpl.java:319)
at com.axelor.apps.supplychain.service.PurchaseOrderServiceSupplychainImpl.requestPurchaseOrder(PurchaseOrderServiceSupplychainImpl.java:311)
at com.google.inject.persist.jpa.JpaLocalTxnInterceptor.invoke(JpaLocalTxnInterceptor.java:64)
at com.axelor.apps.purchase.web.PurchaseOrderController.requestPurchaseOrder(PurchaseOrderController.java:170)
at com.axelor.apps.base.module.ControllerMethodInterceptor.invoke(ControllerMethodInterceptor.java:36)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:566)
at com.axelor.meta.ActionHandler.call(ActionHandler.java:247)
at com.axelor.meta.schema.actions.ActionMethod.evaluate(ActionMethod.java:77)
at com.axelor.meta.schema.actions.Action.execute(Action.java:101)
at com.axelor.meta.schema.actions.Action.wrap(Action.java:114)
at com.axelor.meta.schema.actions.ActionGroup.evaluate(ActionGroup.java:236)
at com.axelor.meta.schema.actions.Action.execute(Action.java:97)
at com.axelor.meta.schema.actions.Action.wrap(Action.java:114)
at com.axelor.meta.ActionHandler.execute(ActionHandler.java:516)
at com.axelor.meta.ActionExecutor.execute(ActionExecutor.java:58)
at com.axelor.rpc.ResponseInterceptor.invoke(ResponseInterceptor.java:61)
at com.axelor.web.service.ActionService.execute(ActionService.java:79)
at com.axelor.rpc.RequestFilter.invoke(RequestFilter.java:56)
at com.axelor.rpc.ResponseInterceptor.invoke(ResponseInterceptor.java:70)
at jdk.internal.reflect.GeneratedMethodAccessor684.invoke(Unknown Source)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:566)
at org.jboss.resteasy.core.MethodInjectorImpl.invoke(MethodInjectorImpl.java:170)
at org.jboss.resteasy.core.MethodInjectorImpl.invoke(MethodInjectorImpl.java:130)
at org.jboss.resteasy.core.ResourceMethodInvoker.internalInvokeOnTarget(ResourceMethodInvoker.java:660)
at org.jboss.resteasy.core.ResourceMethodInvoker.invokeOnTargetAfterFilter(ResourceMethodInvoker.java:524)
at org.jboss.resteasy.core.ResourceMethodInvoker.lambda$invokeOnTarget$2(ResourceMethodInvoker.java:474)
at org.jboss.resteasy.core.interception.jaxrs.PreMatchContainerRequestContext.filter(PreMatchContainerRequestContext.java:364)
at org.jboss.resteasy.core.ResourceMethodInvoker.invokeOnTarget(ResourceMethodInvoker.java:476)
at org.jboss.resteasy.core.ResourceMethodInvoker.invoke(ResourceMethodInvoker.java:434)
at org.jboss.resteasy.core.ResourceMethodInvoker.invoke(ResourceMethodInvoker.java:408)
at org.jboss.resteasy.core.ResourceMethodInvoker.invoke(ResourceMethodInvoker.java:69)
at org.jboss.resteasy.core.SynchronousDispatcher.invoke(SynchronousDispatcher.java:492)
at org.jboss.resteasy.core.SynchronousDispatcher.lambda$invoke$4(SynchronousDispatcher.java:261)
at org.jboss.resteasy.core.SynchronousDispatcher.lambda$preprocess$0(SynchronousDispatcher.java:161)
at org.jboss.resteasy.core.interception.jaxrs.PreMatchContainerRequestContext.filter(PreMatchContainerRequestContext.java:364)
at org.jboss.resteasy.core.SynchronousDispatcher.preprocess(SynchronousDispatcher.java:164)
at org.jboss.resteasy.core.SynchronousDispatcher.invoke(SynchronousDispatcher.java:247)
at org.jboss.resteasy.plugins.server.servlet.ServletContainerDispatcher.service(ServletContainerDispatcher.java:249)
at org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher.service(HttpServletDispatcher.java:60)
at org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher.service(HttpServletDispatcher.java:55)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:623)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:209)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:153)
at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:51)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:178)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:153)
at com.google.inject.servlet.FilterChainInvocation.doFilter(FilterChainInvocation.java:89)
at com.axelor.db.tenants.AbstractTenantFilter.doFilter(AbstractTenantFilter.java:71)
at com.google.inject.servlet.FilterChainInvocation.doFilter(FilterChainInvocation.java:82)
at org.apache.shiro.guice.web.SimpleFilterChain.doFilter(SimpleFilterChain.java:44)
at io.buji.pac4j.filter.SecurityFilter.lambda$doFilter$1(SecurityFilter.java:79)
at org.pac4j.core.engine.DefaultSecurityLogic.perform(DefaultSecurityLogic.java:140)
at com.axelor.auth.pac4j.AxelorSecurityLogic.perform(AxelorSecurityLogic.java:82)
at io.buji.pac4j.filter.SecurityFilter.doFilter(SecurityFilter.java:77)
at org.apache.shiro.guice.web.SimpleFilterChain.doFilter(SimpleFilterChain.java:41)
at org.apache.shiro.web.servlet.AdviceFilter.executeChain(AdviceFilter.java:108)
at org.apache.shiro.web.servlet.AdviceFilter.doFilterInternal(AdviceFilter.java:137)
at org.apache.shiro.web.servlet.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:125)
at org.apache.shiro.guice.web.SimpleFilterChain.doFilter(SimpleFilterChain.java:41)
at org.apache.shiro.web.servlet.AbstractShiroFilter.executeChain(AbstractShiroFilter.java:450)
at org.apache.shiro.web.servlet.AbstractShiroFilter$1.call(AbstractShiroFilter.java:365)
at org.apache.shiro.subject.support.SubjectCallable.doCall(SubjectCallable.java:90)
at org.apache.shiro.subject.support.SubjectCallable.call(SubjectCallable.java:83)
at org.apache.shiro.subject.support.DelegatingSubject.execute(DelegatingSubject.java:387)
at org.apache.shiro.web.servlet.AbstractShiroFilter.doFilterInternal(AbstractShiroFilter.java:362)
at org.apache.shiro.web.servlet.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:125)
at com.google.inject.servlet.FilterChainInvocation.doFilter(FilterChainInvocation.java:82)
at com.axelor.app.internal.AppFilter.doFilter(AppFilter.java:88)
at com.google.inject.servlet.FilterChainInvocation.doFilter(FilterChainInvocation.java:82)
at com.google.inject.persist.PersistFilter.doFilter(PersistFilter.java:94)
at com.google.inject.servlet.FilterChainInvocation.doFilter(FilterChainInvocation.java:82)
at com.axelor.db.tenants.AbstractTenantFilter.doFilter(AbstractTenantFilter.java:71)
at com.google.inject.servlet.FilterChainInvocation.doFilter(FilterChainInvocation.java:82)
at com.axelor.web.servlet.CorsFilter.doFilter(CorsFilter.java:138)
at com.google.inject.servlet.FilterChainInvocation.doFilter(FilterChainInvocation.java:82)
at com.axelor.web.servlet.ProxyFilter.doFilter(ProxyFilter.java:44)
at com.google.inject.servlet.FilterChainInvocation.doFilter(FilterChainInvocation.java:82)
at com.google.inject.servlet.ManagedFilterPipeline.dispatch(ManagedFilterPipeline.java:121)
at com.google.inject.servlet.GuiceFilter.doFilter(GuiceFilter.java:133)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:178)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:153)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:167)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90)
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:481)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:130)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93)
at org.apache.catalina.valves.AbstractAccessLogValve.invoke(AbstractAccessLogValve.java:673)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343)
at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:390)
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63)
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:926)
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1791)
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52)
at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191)
at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.base/java.lang.Thread.run(Thread.java:829)

2025-10-13 09:01:10.062 DEBUG 10842 — [nio-8000-exec-8] c.a.s.service.ws.WsConnectoServiceImpl : Request1: {status=0, data=[{info={message=java.lang.NullPointerException}}]}
2025-10-13 09:01:10.066 DEBUG 10842 — [nio-8000-exec-8] c.a.s.bpm.listener.WkfExecutionListener : Task config searched with taskId: Event_0vm0vd9, processInstanceId: 327598, found:WkfTaskConfig{id=2, name=Event_0vm0vd9, createTask=false, notificationEmail=false}
2025-10-13 09:01:10.068 DEBUG 10842 — [nio-8000-exec-8] c.a.s.bpm.listener.WkfExecutionListener : Task config searched with taskId: Event_0vm0vd9, processInstanceId: 327598, found:WkfTaskConfig{id=2, name=Event_0vm0vd9, createTask=false, notificationEmail=false}
2025-10-13 09:01:10.071 DEBUG 10842 — [nio-8000-exec-8] c.a.s.bpm.listener.WkfExecutionListener : Task config searched with taskId: Event_0vm0vd9, processInstanceId: 327598, found:WkfTaskConfig{id=2, name=Event_0vm0vd9, createTask=false, notificationEmail=false}
2025-10-13 09:01:10.132 DEBUG 10842 — [nio-8000-exec-8] c.a.s.bpm.service.WkfDisplayServiceImpl : Display wkf nodes of processInstanceId: 327598
2025-10-13 09:01:10.135 DEBUG 10842 — [nio-8000-exec-8] c.a.s.bpm.service.WkfDisplayServiceImpl : Is valid model to display wkf nodes : false
2025-10-13 09:01:10.159 DEBUG 10842 — [io-8000-exec-10] c.a.s.bpm.service.WkfDisplayServiceImpl : Display wkf nodes of processInstanceId: 327598
2025-10-13 09:01:10.161 DEBUG 10842 — [io-8000-exec-10] c.a.s.bpm.service.WkfDisplayServiceImpl : Is valid model to display wkf nodes : false

Does anyones see any problem in my business process defintion and how to get it right?

Thank you,
Marius

Error messages been generated during the script execution are often not very useful.
Most likely purchaseOrder is null, but I’d correct this one first:

[purchaseOrder : purchaseOrder]

to
[_purchaseOrder: purchaseOrder.id]
and so the id key value of the request will be: $_purchaseOrder

Also please take a look at this one:
Working BPM script example

Hi Serge,

Thank you for your swift reply. Your article Working BPM script example is very useful and helped me when I first put together my BPM model.
However, the change to [_purchaseOrder: purchaseOrder.id] has produced the same NullPointerException with the exact same stack trace.
In tcpdump I get the request I’m sending to the server. That is:
{« data »:{« context »:{« _model »:« com.axelor.apps.purchase.db.PurchaseOrder »,« id »:337}},« action »:« action-purchase-order-method-requested »,« model »:« com.axelor.apps.purchase.db.PurchaseOrder »}
That is corect. But the response is:
{« status »:0,« data »:[{« info »:{« message »:« java.lang.NullPointerException »}}]}
Even more surprisingly, the request works well when run from Postman.
I hope I’ve norrowed down a bit what happens.

Well, that’s interesting. The only difference between that request and the one that Postman sends can be in id value representation. The server side logic cannot find object with the id from the request and thus falls into NPE.
It would be useful to know what is finally turned out to be the reason of this NPE.

1 « J'aime »

Ah, I remembered. I was getting the same NPE with wrong credentials in the Connector’s Authenticator. That’s why Postman is working normally, but the script fails.

After further digging into the issue and some help from ChatGPT, I have come to the following conclusion.
The whole thing seems to be related to transactions. Here, we are in a transaction started by the Save operation. At the moment my script task that calls the connector starts, we are in this transaction, which is not yet commited. We can access the entities (PurchaseOrder in our case) in any way: purchaseOrder, $ctx.find(‹ PurchaseOrder ›, purchaseOrderId) or $ctx.getRepository(‹ PurchaseOrder ›).find(puchaseOrderId). Sorry if the syntax is incorect, I only aim to express my idea. So the entity seems to be well materialized. But it’s not. If, in my script, insted of the post request which calls the action-purchase-order-method-requested action, I call a GET request for PurchaseOrderId, the backend’s response is { status : 0 }, which means no record hase been found. But if a call the same GET request for another PurchaseOrder which was previously in the database, the request works. The same works for other objects suach as Product or SaleOrder. This means that when we call the connctor we find ouselves in a different transaction, which cannot access the current persistence context and thus cannot find our newly created PurchaseOrder. This is why, in the controller the line Beans.get(PurchaseOrderRepository.class).find(purchaseOrder.getId())) retrieves nothing, passes Null to the service and PurchaseOrderServiceImpl fails with NullPointerException.
Instead, what ChatGPT helped me realize is that I can call the service directly, $beans.get(PurchaseOrderService.class).requestPurchaseOrder, feeding it the actual JPA entity that I can access in my context.
This time around the whole thing works. But the solution is not robust enough for a few reasons. First, Axelor’s best practice is to call the backend via its API. Second, bypassing the API we also bypass the controller which, in some cases, may perform non-trivial things other than calling the service. Third, ‹ requestPurchaseOrder › is only a first step in my process and the more steps I add the longer a single transaction will get. Again, against best practices.
I note also that I tried to bypass the connector but not the API call. That is I wrote low level code using wslite to call the API. The result was the same NullPointerException.
Now that the problem is further isolated, it seems that I need to defer the execution of my connector script until after the purchase order has been saved (comitted to the database).
I have tried different things: flushing the persistence context before calling the connector, explicitely calling em.getTransaction().commit(), starting the process not with a none event but with a conditional event, inserting a user task waiting for some condition and finally adding camunda: asyncBefore to either the script task or the process’s start event. Nothing works.
Does anywone has an idea how to defer the execution of the process or at list of the script task after the creation of the PurchaseOrder is commited?

Thank you,
Marius

Thanks again but the credentials are good, tested on other requests. You can see my comments on the main thread. In Postman, I’ve realized, it woks because I send the request moments later so the ‹ save › transaction has already commited.

Hm… I’d add a pause to the script with something like
TimeUnit.SECONDS.sleep(2);
just for testing purposes. This will show clearly if it is really a delay somewhere in the JPA engine.
And if confirmed it is worth to try to profile script under debugger.

No, it doesn’t change anything. I get the same NullPointerException.

Well. I’ve got the error saving record from the script also.

java.lang.IllegalArgumentException: org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [com.axelor.meta.db.MetaJsonRecord#141]

It seems to be due to the multithreading, supposingly related with the createdBy or updatedBy fields (having CascadeType.PERSIST, CascadeType.MERGE) of the model because of

java.util.concurrent.ExecutionException: java.lang.IllegalStateException: javax.persistence.PersistenceException: org.hibernate.PersistentObjectException: detached entity passed to persist: com.axelor.auth.db.User

The object itself is accessible inside the process, but it is really not persisted to the database until the process is finished.
All this is probably due to the specific Camunda architecture and they even warns about it

You should not trust ACID transaction managers to glue together the workflow engine with your business code.

camunda: no transaction managers

So I still cannot save the object inside the script task too.
Very interesting case.

1 « J'aime »

Moreover, this code causes script task to fail with error

jdbc connection is lost

def _newRecObj = __ctx__.create('books')
def src = books
_newRecObj.title = src.title
_newRecObj.author = src.author
_newRecObj.isbn = src.isbn

__log__.error("M.0 - "+_newRecObj.title+", "+_newRecObj.author+", "+_newRecObj.isbn)

def _newRec = __ctx__.save(_newRecObj)

Meanwhile documentation does not specify any limitations on save operations:
Les builders du BPM - Variables disponibles pour les scripts

Before the jdbc connection is lost, I can see in console the message M.0 with correct values.
And despite of error messages the MetaJsonRecord table is flooded with identical records:

What is going on :slight_smile: ?

1 « J'aime »

And finally. What can we do aiming to make a quick check and not to dig into the source code deep?
Unwraping the session and changing the lock mode come into play:

def _newRecObj = __ctx__.create('books')
def sess = com.axelor.db.JPA.em().unwrap(org.hibernate.Session.class)
def _LockMode = org.hibernate.LockMode.PESSIMISTIC_WRITE
def jpaObject = (com.axelor.db.Model) _newRecObj.getTarget()
sess.buildLockRequest(new org.hibernate.LockOptions(_LockMode)).lock(jpaObject)

def src = books
_newRecObj.title = src.title
_newRecObj.author = src.author
_newRecObj.isbn = src.isbn

__log__.error("M.0 - "+_newRecObj.title+", "+_newRecObj.author+", "+_newRecObj.isbn)

def _newRec = __ctx__.save(_newRecObj)
//def _result = __ctx__.createObject(_newRec)
return books

And this time error message is much more informative:

Caused by: java.lang.IllegalArgumentException: org.hibernate.TransientObjectException: cannot lock an unsaved transient instance: com.axelor.meta.db.MetaJsonRecord

That means, that somewhere in the pesistense-related code we will find annotations, that should have

cascade=CascadeType.ALL

but they do not.

1 « J'aime »

Well. I think I have found the root cause of this case.
Camunda data flow is multithreaded by design, it’s data flow is described there: Data flow description
But Hibernate sessions are not thread-safe and here what is happening:

  1. A simple process, that is starting at Book entity creation

    It has simple arbitrary ISBN check just for convenience.

    At this stage we do not try to save the entity, just adding a semicolon to the title (some sort of visual counter)
    Save the Book and checking process flow:


    Finalize the Book entity, changing the ISBN field to ensure it contains 99 and save:

    As the title field has the one semicolon now, script is executed once. And there is no errors in the console.
  2. Multithreading effect ( Camunda workers, jobs? )
    Changing the script a bit according to documentation, adding call to the save() method.

    Create new Book entity and try to save:

    Here we have 4 excessive script calls (4 semicolons added) and errors in the console are:

Caused by: java.lang.IllegalArgumentException: javax.persistence.OptimisticLockException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [com.axelor.meta.db.MetaJsonRecord#306]

Yes, Camunda engine uses optimistic locks.
3. How to avoid it
So given all this, let’s change the Save script to:

if ( !books.title.matches("/ : /") ) {
books.title = books.title + " : "
    __log__.error("TNX.book - "+books.title)

    def jpaObject = (com.axelor.db.Model) books.getTarget()
    __log__.error("TNX.obj - "+jpaObject)

    def _em = com.axelor.db.JPA.em()

    if ( !_em.contains(jpaObject) ) {
        __log__.error("TNX.before merge - "+jpaObject)
        _em.merge(jpaObject)
    }
}
// Checking if entity persisted
def _listBooks = __ctx__.filter(MetaJsonRecord.class.getName(),
             "self.jsonModel like :modelName", [modelName:'books'])

counter = 1
for(elem : _listBooks) {
__log__.error("E."+counter+" - id="+elem.id+", attrs: "+elem.attrs)
counter++
}

If statement prevents excessive calls (this time we do not need them to occure), and the last query is to ensure entity is persisted correctly.
Create new Book entity and save:


The resulting process flow, looks good:

Console log contains no errors (except those ones, intentionally printed from the script :wink:), we can see the correct Book entity immediately after merge in place:

2025-10-21 17:26:12.280 DEBUG 4126 — [l-7948-thread-1] c.a.s.bpm.listener.WkfExecutionListener : Executing: id=Activity_1en1zab,name=Check_ISBN
2025-10-21 17:26:12.281 DEBUG 4126 — [l-7948-thread-1] c.a.s.bpm.listener.WkfExecutionListener : Task config searched with taskId: Activity_1en1zab, processInstanceId: 4946, found:WkfTaskConfig{id=96, name=Activity_1en1zab, createTask=false, taskNameType=value}
2025-10-21 17:26:12.292 DEBUG 4126 — [l-7948-thread-1] c.a.s.b.s.e.WkfInstanceServiceImpl : Model process instanceId added: 4946
2025-10-21 17:26:12.298 DEBUG 4126 — [l-7948-thread-1] c.a.s.b.s.execution.WkfTaskServiceImpl : Variable map used: {books=com.axelor.utils.helpers.context.FullContext@4a2bf164}
2025-10-21 17:26:12.299 DEBUG 4126 — [l-7948-thread-1] c.a.s.bpm.service.WkfCommonServiceImpl : Process variables: {books=ObjectValue [value=com.axelor.utils.helpers.context.FullContext@4a2bf164, isDeserialized=true, serializationDataFormat=application/json, objectTypeName=null, serializedValue=null, isTransient=true], booksId=Value ‹ 307 › of type ‹ PrimitiveValueType[long] ›, isTransient=false}
2025-10-21 17:26:12.301 DEBUG 4126 — [l-7948-thread-1] c.a.s.bpm.service.WkfCommonServiceImpl : Process variables: {}
2025-10-21 17:26:12.315 DEBUG 4126 — [l-7948-thread-1] c.a.s.bpm.service.WkfCommonServiceImpl : Eval expr:
books?.isbn?.contains(‹ 99 ›) , result: true
2025-10-21 17:26:12.315 DEBUG 4126 — [l-7948-thread-1] c.a.s.b.s.execution.WkfTaskServiceImpl : Valid expr:
books?.isbn?.contains(‹ 99 ›)
2025-10-21 17:26:12.324 DEBUG 4126 — [l-7948-thread-1] c.a.s.bpm.listener.WkfExecutionListener : Task config searched with taskId: Activity_1en1zab, processInstanceId: 4946, found:WkfTaskConfig{id=96, name=Activity_1en1zab, createTask=false, taskNameType=value}
2025-10-21 17:26:12.325 DEBUG 4126 — [l-7948-thread-1] c.a.s.bpm.listener.WkfExecutionListener : Executing: id=Gateway_07amgd7,name=null
2025-10-21 17:26:12.356 DEBUG 4126 — [l-7948-thread-1] c.a.s.bpm.listener.WkfExecutionListener : Executing: id=Activity_1tkqc4i,name=Saving book entity within the script
2025-10-21 17:26:12.407 ERROR 4126 — [l-7948-thread-1] org.camunda.bpm.engine.script : TNX.book - The 6th Book :
2025-10-21 17:26:12.408 ERROR 4126 — [l-7948-thread-1] org.camunda.bpm.engine.script : TNX.obj - MetaJsonRecord{id=307, jsonModel=books, name=The 6th Book}
2025-10-21 17:26:12.409 ERROR 4126 — [l-7948-thread-1] org.camunda.bpm.engine.script : TNX.before merge - MetaJsonRecord{id=307, jsonModel=books, name=The 6th Book}
2025-10-21 17:26:12.423 ERROR 4126 — [l-7948-thread-1] org.camunda.bpm.engine.script : E.1 - id=300, attrs: {« isbn »: « 994247330 », « title »: "The 2nd Book : ", « author »: « The 2nd Author »}
2025-10-21 17:26:12.423 ERROR 4126 — [l-7948-thread-1] org.camunda.bpm.engine.script : E.2 - id=306, attrs: {« isbn »: « 9904447333 », « title »: "The 5th Book : : : : : ", « author »: « The 5th Author »}
2025-10-21 17:26:12.423 ERROR 4126 — [l-7948-thread-1] org.camunda.bpm.engine.script : E.3 - id=14, attrs: {« isbn »: « 9981433215452 », « title »: « The next 100 years », « author »: « George Friedman », « noteText »: « George Friedman The next 100 years », « noteDescription »: « The next 100 years »}
2025-10-21 17:26:12.423 ERROR 4126 — [l-7948-thread-1] org.camunda.bpm.engine.script : E.4 - id=308, attrs: {« title »:"The 6th Book : ",« author »:« The 6th Author »,« isbn »:« 1111753499 »}
2025-10-21 17:26:12.423 ERROR 4126 — [l-7948-thread-1] org.camunda.bpm.engine.script : E.5 - id=2, attrs: {« isbn »: « 123476599 », « title »: « The River’s world », « author »: « Philip Farmer », « noteText »: « Philip Farmer The River’s world », « noteDescription »: « The River’s world »}
2025-10-21 17:26:12.423 ERROR 4126 — [l-7948-thread-1] org.camunda.bpm.engine.script : E.6 - id=302, attrs: {« isbn »: « 5553422199 », « title »: « The second book », « author »: « Tee view test »}
2025-10-21 17:26:12.423 ERROR 4126 — [l-7948-thread-1] org.camunda.bpm.engine.script : E.7 - id=304, attrs: {« isbn »: « 432187599 », « title »: "The 4th Book : ", « author »: « The 4th Author »}
2025-10-21 17:26:12.423 ERROR 4126 — [l-7948-thread-1] org.camunda.bpm.engine.script : E.8 - id=303, attrs: {« isbn »: « 432187599 », « title »: "The 4th Book : ", « author »: « The 4th Author »}
2025-10-21 17:26:12.423 ERROR 4126 — [l-7948-thread-1] org.camunda.bpm.engine.script : E.9 - id=299, attrs: {« isbn »: « 9904447333 », « title »: « The 3rd Book », « author »: « The 3rd Author »}
2025-10-21 17:26:12.424 DEBUG 4126 — [l-7948-thread-1] c.a.s.bpm.listener.WkfExecutionListener : Executing: id=Event_19y21be,name=null

  1. Conclusions.
    Camunda uses its own transaction managment setting optimistic locks. Axelor context persistence helpers has pitfalls due to the fact that hibernate sessions are not thread-safe. While this workaround can do it’s job, it is just a quick and dirty solution to highlight the problem root cause.
1 « J'aime »

Hi, Serge. I’ve read all your investigations that show clearly the beahviour si different from what we expect. Also, they are useful for further develpments of business processes. What I also did was to make sure that the PurchaseOrder object is not saved before the script task goes into action. I put a TimeUnit.MINUTES.sleep(3) and while the script was ‹ sleeping › I checked in the Postgresql database whether the PurchaseOrder was there. It was not. After the 3 minutes it was in the database. Also, running a request with Postman returned nothing during the 3 minutes but returned the record after that. This means, when the script starts off, we are still in the ‹ save › transaction. I will describe how I managed to mitigate this on the main thread. Thanks for your detailed replies.

I have managed to defer the execution of the script task after the PurchaseOrder has been saved. To this end I used Camunda’s asynchronous continuations. In my case, it worked like this:

<bpmn2:startEvent id="StartEvent_1" name="Start" camunda:asyncAfter="true">

This is not available in the BPM user interface, at least not in the free version.
Now my business process looks the same


, but ‹ Request purchase order › script task starts on a different thread while the ‹ save › transaction has been committed, that is the PurchaseOrder object is available. Running HTTP requests from the script task is now possible, including retrieving the object through a GET request.
Now I’m facing other issues, on wich I may open new subject(s), but for this thread the problem is solved for me.

1 « J'aime »