Skip to content

Custom Java Delegates

Required Access Level

Super Admin

The Flowi Agentic workflow engine (Flowable) supports the execution of custom logic injected in the middle of processes through Java Delegates. Unlike external integrations (Webhooks), Delegates run natively inside the engine's memory, ensuring performance and atomic transactions with the database.

Why uploading is Super Admin only

A Java Delegate runs inside the platform's own virtual machine, and Java offers no sandbox for code executing in-process. In practice a delegate can reach the database, the environment variables and any application component — no configuration can constrain it.

That is why uploading is restricted to the Super Admin. This role already holds full platform access, so uploading code grants no privilege it did not already have: the trust boundary matches the privilege exactly.

Delegates are not uploaded per tenant, and this is a design decision rather than a pending step. Tenant code that needs to run during a process belongs in Robots, which execute outside the platform's virtual machine, hold no database credentials, and talk to the system only over RPC scoped to a single tenant.

Uploading the Delegate

  1. Access Global (Super Admin) → Java Delegates.
  2. Upload your compiled .jar file.
  3. The system will automatically scan and extract the classes that implement the JavaDelegate interface.
  4. The delegateExpression column shows the exact expression for each class — for example ${parserNotaFiscal}.
  5. In the BPMN Modeler, whoever draws the process picks the delegate from a list, typing nothing — see A step that calls a Java Delegate.

The bean name comes from the class's @Component("name"). Without the annotation, the platform uses the simple class name with a lowercase initial. That name is what the modeler sees, so it is worth choosing with the reader of the diagram in mind.

The delegate list is read only by whoever models

Uploading a JAR is the Super Admin's; reading the list (GET /a/delegates) requires a tenant Admin or a Super Admin. The response carries each bean's Java class name and declared parameters — information about how the installation is built inside — and the only consumer is the modeler's selector, which is already an Admin screen. A Manager or an ordinary user gets 403.

Process and case ask for different interfaces

JavaDelegate is the BPMN engine's interface. The case engine (CMMN) does not accept it: it runs PlanItemJavaDelegate, PlanItemFutureJavaDelegate or CmmnActivityBehavior. One class may implement both and serve both engines.

The platform checks this when it loads the JAR and records, per bean, which engines can run it. That marking is what makes the CMMN modeler list case delegates only and warn when the file points at a process-only delegate — before, the wrong choice showed up only as a step stuck in production.

That list holds what this tenant uploaded. The platform's own case delegate — the AI step — is not in it; it has its own entry in the implementation dropdown. See AI Agents.

Declaring parameters

A delegate that always does exactly the same thing needs no configuration. A useful one usually does: which field to read, which layout to assume, which threshold to apply.

Declare each parameter on the class with @DelegateParameter, the same way a robot uses @RobotParameter:

java
@Component("parserNotaFiscal")
@DelegateParameter(name = "layout", label = "XML layout", required = true, defaultValue = "nfse-v2")
public class ParserNotaFiscal implements JavaDelegate {

    private Expression layout;

    @Override
    public void execute(DelegateExecution execution) {
        String chosen = (String) layout.getValue(execution);
    }
}

The platform reads the annotation while scanning the JAR, without loading the class, and the modeler then sees one field per declared parameter. The value reaches the delegate through Flowable's own field injection: declare an Expression field named after the parameter.

A parameter holds no secret

@DelegateParameter has no secret, and that is deliberate. The value the modeler fills in lives inside the process XML, which is versioned, exportable and readable by anyone who opens the diagram. Marking it secret would be a promise the storage cannot keep. For credentials, read tenant variables at execution time.

Where the annotation comes from

@DelegateParameter and @RobotParameter live in backend/robot-runner, an internal module that is not published to any Maven repository. There is no artifact for you to depend on.

That is deliberate, and the supported path is to declare your own copy. The platform matches the annotation by fully qualified name, reading the JAR's bytes without loading the class — it never compares the type against its own. A copy of yours therefore works identically, as long as the package and the name match exactly:

java
package ia.flow.engine;

import java.lang.annotation.ElementType;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Repeatable(DelegateParameters.class)
public @interface DelegateParameter {
    String name();
    String label() default "";
    boolean required() default false;
    String defaultValue() default "";
}

The repeatable companion (DelegateParameters, holding DelegateParameter[] value()) has to exist alongside it, or the compiler refuses a second annotation on the same class.

Why no published artifact

Publishing would need a Maven repository the customer can reach, credentials, and a version compatibility promise that is not worth keeping yet. While the annotation is five lines with no behaviour, copying is cheaper for both sides — and matching by name means the copy does not age.

What the platform refuses

A bean name already used by the platform. The upload is refused, naming both the class and the conflicting bean. Silently overwriting a system component would change platform behaviour without leaving a trace.

A deploy referencing a delegate that does not exist. Publishing a BPMN that uses ${someDelegate} without that delegate loaded is refused, and the message lists the available delegates. Without this refusal the process would be published, accept work, and only fail at the step — as a job retrying forever.

flowable:class. Always use delegateExpression. The class-name form does not resolve for uploaded JARs.

A JAR that fails to load. If the classes cannot be loaded the upload fails, the file is discarded and the previous version stays active. If the failure happens at platform startup, the delegate is shown as Not loaded instead of appearing active.

Replacing a version

Uploading a new JAR replaces the bean with the new version. Running processes pick it up on the next step that calls it, so a long execution may span two versions.

Flowi Agentic — Plataforma de Gestão de Processos com IA