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

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
}
}
src/main/resources/specs/ui/routes.yaml
type: Routes
routes:
- route: orders/new
viewModel: com.example.CreateOrderPage

public class OrderWorkflowPage implements ComponentTreeSupplier, ActionHandler, TriggersSupplier {
@Override
public Form component(HttpRequest httpRequest) {
String workflowId = httpRequest.lastPathItem(); // orders/:workflowId
var state = workflowEngine.getState(workflowId);
return Form.builder()
.title("Order " + workflowId)
.content(List.of(
new Text("Status: " + state.currentStep()),
ProgressBar.builder().value(state.progress()).build()
// + step-specific form fields based on state.currentStep()
))
.build();
}
@Override
public List<Trigger> triggers(HttpRequest httpRequest) {
return List.of(new OnLoadTrigger("refresh"));
}
@Override
public Object handleAction(String actionId, HttpRequest httpRequest) {
// "refresh": re-render with the latest workflow state
return component(httpRequest);
}
}
src/main/resources/specs/ui/routes.yaml
type: Routes
routes:
- route: orders/:workflowId
viewModel: com.example.OrderWorkflowPage

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.lastPathItem(); // the :step segment
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).