Skip to content

Workflow and forms integration

Mateu acts as the UI layer for workflow and form-driven processes. It triggers workflow steps, collects user input through forms, and displays progress — while the workflow engine handles orchestration.

Prerequisite: understand actions and UI effects — workflows use URI navigation between steps and State updates to reflect progress. For long-running steps, actions can stream progress via SSE.


User → Mateu UI → Workflow engine → Backend services
↑ |
└──── status updates ──────┘

The Mateu page:

  1. Shows the current workflow state
  2. Collects user input (a form)
  3. Submits input to the workflow engine via an action
  4. Displays the updated state or next step

@Route(value = "/orders/new", parentRoute = "")
public class CreateOrderPage implements ComponentTreeSupplier, ActionHandler {
String customerId;
String notes;
@Override
public Form component(HttpRequest httpRequest) {
return Form.builder()
.title("New order")
.toolbar(List.of(
Button.builder().label("Submit").actionId("submit").build()
))
.content(List.of(
FormField.builder().id("customerId").label("Customer")
.dataType(FieldDataType.string)
.stereotype(FieldStereotype.combobox)
.build(),
FormField.builder().id("notes").label("Notes")
.dataType(FieldDataType.string)
.stereotype(FieldStereotype.textarea)
.build()
))
.build();
}
@Override
public Object handleAction(String actionId, HttpRequest httpRequest) {
customerId = httpRequest.getString("customerId");
notes = httpRequest.getString("notes");
String workflowId = workflowEngine.start("create-order", Map.of(
"customerId", customerId,
"notes", notes
));
return URI.create("/orders/" + workflowId); // navigate to the workflow detail page
}
}

@Route(value = "/orders/:workflowId", parentRoute = "")
public class OrderWorkflowPage implements ComponentTreeSupplier, ActionHandler, TriggersSupplier {
@Override
public Form component(HttpRequest httpRequest) {
String workflowId = httpRequest.getPathVariable("workflowId");
var state = workflowEngine.getState(workflowId);
return Form.builder()
.title("Order " + workflowId)
.content(List.of(
new Text("Status: " + state.currentStep()),
new ProgressBar.builder().value(state.progress()).build(),
// show step-specific form fields based on state.currentStep()
))
.build();
}
@Override
public List<Trigger> triggers(HttpRequest httpRequest) {
return List.of(new OnLoadTrigger("refresh"));
}
}

For steps that take time, stream progress updates using SSE:

Action.builder()
.id("process")
.sse(true)
.build()
@Override
public Object handleAction(String actionId, HttpRequest httpRequest) {
return Flux
.fromIterable(workflowEngine.executeSteps("order-processing"))
.map(step -> new State(Map.of(
"currentStep", step.name(),
"progress", step.progressFraction()
)));
}

Each emitted value updates the browser progressively. The user sees live progress without polling.


Complex workflows with multi-step forms can use a different page (or the same page rendering different content) for each step:

@Override
public Form component(HttpRequest httpRequest) {
String step = httpRequest.getPathVariable("step");
return switch (step) {
case "customer" -> renderCustomerStep();
case "items" -> renderItemsStep();
case "payment" -> renderPaymentStep();
case "confirm" -> renderConfirmStep();
default -> renderCustomerStep();
};
}

Each step returns the user’s input to the next step’s page via URI.create("/order/new/" + nextStep).