### startsWith Source: https://github.com/lisegmbh/fluxflow/blob/develop/docs/features/queries.md Tests if a string starts with a given prefix. Case sensitivity can be specified. Requires import. ```APIDOC ## startsWith ### Description Tests if a string starts with a given prefix. You can also specify case sensitivity. ### Import `import de.fluxflow.flowquery.expression.ExpressionExtensions.Strings.startsWith` ### Usage ```kotlin query.where { get(User::email).startsWith("admin@") // With case sensitivity: get(User::email).startsWith("ADMIN@", ignoreCasing = false) } ``` ``` -------------------------------- ### Start a Workflow with a Model Instance Source: https://github.com/lisegmbh/fluxflow/blob/develop/docs/workflow/model.md Initiate a workflow by passing an instance of your workflow model to the WorkflowStarterService.start function. The provided model instance will be used as the workflow's state. ```kotlin import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController import java.time.Instant @RestController @RequestMapping("/api/vacationrequest") class VacationRequestController( private val workflowStarterService: WorkflowStarterService ) { @PostMapping fun createRequest() { workflowStarterService.start( VacationRequestWorkflow( Instant.now() ), Continuation.none() ) } } ``` -------------------------------- ### Multiple Continuation Example Source: https://github.com/lisegmbh/fluxflow/blob/develop/docs/steps.md Use `Continuation.multiple` to execute multiple operations concurrently after a step completes. Wrap individual continuations using `Continuation.step`. ```kotlin class NotifyCustomerStep { /* ... */ } class NotifyRestaurantStep { /* ... */ } class SubmitPizzaOrderStep { fun submit(): Continuation<*> { return Continuation.multiple( // Continuation.step(NotifyCustomerStep()), // Continuation.step(NotifyRestaurantStep()) ) } } ``` -------------------------------- ### Start a Workflow from a Spring Boot Controller Source: https://github.com/lisegmbh/fluxflow/blob/develop/README.md Initiate a FluxFlow workflow by injecting `WorkflowEngine` into a Spring Boot REST controller and calling its `start` method. ```kotlin @RestController class PizzaOrderController( private val workflowEngine: WorkflowEngine ) { @PostMapping("/pizza-orders") fun startPizzaOrder(): String { val workflow = workflowEngine.start(AddPizzaToCartStep()) return workflow.id } } ``` -------------------------------- ### Start a New Workflow Instance with WorkflowEngine Source: https://context7.com/lisegmbh/fluxflow/llms.txt Inject `WorkflowEngine` into a Spring component to start a new workflow instance. The engine persists the initial step's state and returns the workflow object. ```kotlin import de.lise.fluxflow.api.WorkflowEngine import de.lise.fluxflow.api.continuation.Continuation import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController @RestController @RequestMapping("/api/ideas") class IdeaController( private val workflowEngine: WorkflowEngine ) { @PostMapping fun createIdea(): Map { // Start a new workflow; the engine persists state and returns the workflow object val workflow = workflowEngine.start(SubmitIdeaStep()) return mapOf("workflowId" to workflow.id) // Response: { "workflowId": "64f3a1b2c3d4e5f6a7b8c9d0" } } } ``` -------------------------------- ### Create Setup/Teardown Job Execution Interceptor Source: https://github.com/lisegmbh/fluxflow/blob/develop/docs/jobs.md Implement a JobExecutionInterceptor to perform setup actions before calling token.next() and teardown actions after it returns. Ensure teardown actions are correctly handled based on job execution status. ```kotlin import de.lise.fluxflow.api.interceptors.InterceptionToken import de.lise.fluxflow.api.job.Job import de.lise.fluxflow.api.job.interceptors.JobExecutionContext import de.lise.fluxflow.api.job.interceptors.JobExecutionInterceptor import org.slf4j.LoggerFactory import org.springframework.stereotype.Component @Component class CustomJobExecutionSetupInterceptor : JobExecutionInterceptor { private val log = LoggerFactory.getLogger(CustomJobExecutionSetupInterceptor::class.java)!! override fun intercept(token: InterceptionToken) { setup(token.context.job) try { token.next() } finally { teardown() } } private fun setup( job: Job ) { log.debug("Setting up execution context for job '{}'", job.identifier) // custom setup actions } private fun teardown() { /** custom teardown actions **/ } } ``` ```kotlin override fun intercept(token: InterceptionToken) { setup(token.context.job) when(token.next()) { InterceptionTokenStatus.Executed -> teardown() else -> {} } } ``` -------------------------------- ### Workflow Continuation Source: https://github.com/lisegmbh/fluxflow/blob/develop/docs/steps.md Initiate a new workflow using `Continuation.workflow`. This allows for starting independent workflows, optionally passing data and specifying how the current workflow should behave. ```kotlin class SubmitPizzaOrderStep { fun submit(): Continuation<*> { val feedback = CustomerFeedback() return Continuation.workflow( // feedback, Continuation.step(feedback) ) } } class CustomerFeedback {/** ...**/ } class AskForFeedbackStep( feedback: CustomerFeedback ) { /** ... **/ } ``` -------------------------------- ### Define a Review Step Source: https://github.com/lisegmbh/fluxflow/blob/develop/docs/getting-started/getting-started.md Annotate a class with @Step to define a workflow step. This example shows a simple review step. ```kotlin package de.lise.fluxflow.examples.ideas.workflow import de.lise.fluxflow.stereotyped.step.Step @Step class IdeaReviewStep { } ``` -------------------------------- ### FluxFlow Workflow Model and Starter Service Source: https://context7.com/lisegmbh/fluxflow/llms.txt Defines a workflow model with shared data and a listener for data changes. Shows how to start a new workflow instance using WorkflowStarterService with an initial model and step. ```kotlin import de.lise.fluxflow.api.continuation.Continuation import de.lise.fluxflow.api.workflow.WorkflowStarterService import de.lise.fluxflow.stereotyped.workflow.model.ModelListener import java.time.Instant enum class RequestStatus { Draft, Approved, Rejected } data class VacationRequest( val employeeId: String, val createdAt: Instant = Instant.now(), var status: RequestStatus = RequestStatus.Draft ) { // Model listener: invoked only when `status` actually changes @ModelListener("#root.status") fun onStatusChanged() { println("Status changed to $status for employee $employeeId") } } // Starting a workflow with an explicit model and initial step @Service class VacationService(private val starter: WorkflowStarterService) { fun submitRequest(employeeId: String): String { val workflow = starter.start( VacationRequest(employeeId), // workflow model Continuation.step(ReviewRequestStep()) // initial step ) return workflow.id } } ``` -------------------------------- ### Simplify with Action Parameter Injection Source: https://github.com/lisegmbh/fluxflow/blob/develop/docs/steps.md This example shows a functionally identical workflow that uses action parameter injection. The MailService is injected directly into the completeOrder action function, avoiding the need for constructor injection and simplifying upstream dependencies. ```kotlin @Service class MailService { fun sendMail(receiver: String, message: String) {} } @Step class ReviewShoppingCart( val customerMailAddress: String ) { @Action fun proceedToCheckout(): CompleteOrderStep { return CompleteOrderStep(customerMailAddress) } } @Step class CompleteOrderStep( val customerMailAddress: String ) { @Action fun completeOrder(mailService: MailService) { mailService.sendMail(customerMailAddress, "Thank you for your order") } } ``` -------------------------------- ### Automation Function with Constructor Logic Illustration Source: https://github.com/lisegmbh/fluxflow/blob/develop/docs/features/automation.md Avoid using a step's constructor for state-modifying logic as it may be invoked multiple times unpredictably. This example illustrates when logic is executed, showing the constructor's behavior versus an @OnCreated automation function. ```kotlin class CompleteOrderStep { val orderId: String constructor(orderId: String) { this.orderId = orderId // Logger.debug("An instance of CompleteOrderStep for order '{}' created.", orderId) } @OnCreated // fun logOrderAboutToComplete() { Logger.info("Waiting for completion of order '{}'.", orderId) } @Action // fun complete() { Logger.info("Order '{}' has been completed.", orderId) } } ``` -------------------------------- ### Build Type-Safe Queries with FlowQuery API Source: https://context7.com/lisegmbh/fluxflow/llms.txt Construct type-safe queries for workflows, steps, and jobs using the FlowQuery API. This example demonstrates filtering by city and total amount, sorting by total amount, and pagination. ```kotlin import de.fluxflow.flowquery.FlowQuery import de.fluxflow.flowquery.expression.ExpressionExtensions.Logical.and import de.fluxflow.flowquery.expression.ExpressionExtensions.Strings.contains import de.fluxflow.flowquery.expression.ExpressionExtensions.Collections.containsElementThat import de.fluxflow.flowquery.sort.Sorting import de.lise.fluxflow.api.workflow.query.WorkflowQueryable import org.springframework.stereotype.Service data class PizzaOrder( val city: String, val totalAmount: Double, val toppings: List, val customerEmail: String ) @Service class OrderQueryService( private val workflowQueryService: de.lise.fluxflow.api.workflow.WorkflowQueryService ) { fun findHighValueOrdersInCity(city: String): List> { val query = FlowQuery.of() .where { // Combine conditions with `and` extension get(WorkflowQueryable::model).get(PizzaOrder::city).isEqual(city) and get(WorkflowQueryable::model).get(PizzaOrder::totalAmount).isGreaterThan(50.0) } .sort { Sorting.desc(get(WorkflowQueryable::model).get(PizzaOrder::totalAmount)) } .paged(pageIndex = 0, pageSize = 20) return workflowQueryService.findAll(query) } fun findOrdersWithExtraTopping(topping: String) = FlowQuery.of() .where { get(WorkflowQueryable::model) .get(PizzaOrder::toppings) .containsElementThat { isEqual(topping) } } .let { workflowQueryService.findAll(it) } fun findOrdersByEmailDomain(domain: String) = FlowQuery.of() .where { get(WorkflowQueryable::model) .get(PizzaOrder::customerEmail) .contains("@$domain") } .let { workflowQueryService.findAll(it) } } ``` -------------------------------- ### Audit Job Execution with an Interceptor Source: https://context7.com/lisegmbh/fluxflow/llms.txt Implement `JobExecutionInterceptor` to log job execution start and end. The `intercept` method receives an `InterceptionToken` which must be advanced using `token.next()` to continue the execution chain. ```kotlin import de.lise.fluxflow.api.interceptors.InterceptionToken import de.lise.fluxflow.api.interceptors.InterceptionTokenStatus import de.lise.fluxflow.api.job.interceptors.JobExecutionContext import de.lise.fluxflow.api.job.interceptors.JobExecutionInterceptor import org.slf4j.LoggerFactory import org.springframework.stereotype.Component @Component class AuditJobInterceptor : JobExecutionInterceptor { private val log = LoggerFactory.getLogger(AuditJobInterceptor::class.java)!! override fun intercept(token: InterceptionToken) { val jobId = token.context.job.identifier log.info("Starting job execution: {}", jobId) val status = token.next() // proceed with execution chain when (status) { InterceptionTokenStatus.Executed -> log.info("Job {} completed successfully", jobId) else -> log.warn("Job {} was aborted or skipped", jobId) } } } ``` -------------------------------- ### Assigning Step Data with Context Source: https://github.com/lisegmbh/fluxflow/blob/develop/docs/steps.md The setValue function accepts an additional context parameter, which can be accessed by FlowListeners via FlowEvent.context. This example demonstrates passing a User as context. ```kotlin import de.lise.fluxflow.api.step.stateful.data.Data import de.lise.fluxflow.api.step.stateful.data.StepDataService import java.time.Instant /** * Provide additional context information */ class ModificationHistoryService(private val stepDataService: StepDataService) { fun assignLastModifiedDate(data: Data, currentUser: User) { stepDataService.setValue( currentUser, // context information data, // data to be updated Instant.now() // value to be assigned ) } } ``` ```kotlin import de.lise.fluxflow.api.event.FlowEvent import de.lise.fluxflow.api.listener.FlowListener import org.slf4j.LoggerFactory /** * Access additional context information */ class HistoryService: FlowListener { private val logger = LoggerFactory.getLogger(HistoryService::class.java) override fun onFlowEvent(event: FlowEvent) { val modifyingUser = event.context as? User if (modifyingUser != null) { logger.debug("Workflow updated by user: {}", modifyingUser) } } } ``` -------------------------------- ### Customizing Metadata Property Names in Kotlin Source: https://github.com/lisegmbh/fluxflow/blob/develop/docs/steps.md Demonstrates how to use `@get:Metadata` to specify custom property names for metadata extracted from annotation properties. This allows fine-grained control over metadata keys. ```kotlin @Metadata("displayName") annotation class DisplayName( @get:Metadata("long") val longName: String, @get:Metadata("short") val shortName: String ) ``` -------------------------------- ### FluxFlow Step Continuation Types Source: https://context7.com/lisegmbh/fluxflow/llms.txt Demonstrates different ways to control workflow execution flow using Continuation values within a step. Includes implicit and explicit step continuation, no-operation, forking, rollback, and starting new workflows. ```kotlin import de.lise.fluxflow.api.continuation.Continuation import de.lise.fluxflow.api.continuation.ForkBehavior import de.lise.fluxflow.stereotyped.step.Step import de.lise.fluxflow.stereotyped.step.action.Action @Step class OrderStep(val orderId: String) { // 1. Step continuation (implicit): return the next step instance directly @Action fun approve(): PaymentStep = PaymentStep(orderId) // 2. Step continuation (explicit) @Action fun approveExplicit(): Continuation = Continuation.step(PaymentStep(orderId)) // 3. No-op: step completes, no new step is started @Action fun archive(): Continuation<*> = Continuation.none() // 4. Multiple: fork into parallel steps @Action fun notifyAll(): Continuation<*> = Continuation.multiple( Continuation.step(NotifyCustomerStep(orderId)), Continuation.step(NotifyWarehouseStep(orderId)) ) // 5. Rollback: reactivate the previous step @Action fun cancel(): Continuation<*> = Continuation.rollback() // 6. New workflow: fork into a separate workflow process @Action fun startFeedbackFlow(): Continuation<*> { val model = FeedbackWorkflow(orderId) return Continuation.workflow(model, Continuation.step(AskFeedbackStep(model)), ForkBehavior.Fork) } } ``` -------------------------------- ### Map Get Operator Source: https://github.com/lisegmbh/fluxflow/blob/develop/docs/features/queries.md The 'get' operator for maps accesses a value in a map by key. It can be used with a constant key or a key derived from another expression. This is an extension function available for maps. ```APIDOC ## get (Extension) ### Description Accesses a value in a map by key. ### Import `import de.fluxflow.flowquery.expression.ExpressionExtensions.Maps.get` ### Usage ```kotlin query.where { root -> get(User::attributes).get(const("role")).isEqual("admin") // Or with a dynamic key: get(User::attributes).get(other.get(Setting::key)).isEqual("value") } ``` ### Parameters This operator does not take explicit parameters in the markdown format, but it is used with a key argument. ### Example See Usage section for examples. ``` -------------------------------- ### Apply Validation Annotations to Step Data Source: https://github.com/lisegmbh/fluxflow/blob/develop/docs/features/validation.md Use standard Jakarta Bean Validation annotations like `@field:NotBlank` and `@get:AssertTrue` on step properties or getters. Annotations must be prefixed with `@field:` or `@get:` for Kotlin constructor properties. ```kotlin @Step class SubmitContactFormStep( @field:NotBlank // @Data var firstname: String = "", @get:NotBlank @Data var lastname: String = "" ) { private var _acceptedTermsAndConditions: Boolean = false @get:AssertTrue // var acceptTermsAndConditions: get() { return _acceptedTermsAndConditions } set(value) { _acceptedTermsAndConditions = true } // ... } ``` -------------------------------- ### get Operator (Property) Source: https://github.com/lisegmbh/fluxflow/blob/develop/docs/features/queries.md Accesses the value of a specified property. ```APIDOC ## get (Property) ### Description Accesses property value. ### Method Direct method ### Import Required No ``` -------------------------------- ### Define Review Step with Constructor Parameters Source: https://github.com/lisegmbh/fluxflow/blob/develop/docs/getting-started/getting-started.md Define a workflow step that accepts data through its constructor. This allows for passing data from a previous step. ```kotlin package de.lise.fluxflow.examples.ideas.workflow import de.lise.fluxflow.stereotyped.step.Step @Step class IdeaReviewStep( val summary: String, val author: String, val description: String ) { } ``` -------------------------------- ### Define Pizza Ordering Workflow Steps (Kotlin) Source: https://github.com/lisegmbh/fluxflow/blob/develop/README.md Define the workflow steps for adding pizzas to a cart and proceeding to checkout. Includes data classes for Pizza and OrderConfirmation. ```kotlin // 1️⃣ Add Pizza to Cart Step @Step class AddPizzaToCartStep( @Data @NotEmpty(message = "Cart cannot be empty. Please add at least one pizza before checkout.") var pizzas: MutableList = mutableListOf() ) { @Action(beforeExecutionValidation = ValidationBehavior.AllowInvalid) fun addPizza(pizza: Pizza): AddPizzaToCartStep { // Add the pizza to cart pizzas.add(pizza) // Return to same step for more pizzas return AddPizzaToCartStep(pizzas) } @Action fun toCheckout(): CheckoutStep { // Calculate total and proceed to checkout with current pizzas val total = pizzas.sumOf { it.price } return CheckoutStep(pizzas, total) } } // 2️⃣ Checkout Step @Step class CheckoutStep( @Data val pizzas: List, @Data val total: BigDecimal ) { @Data var deliveryAddress: String = "" @Data var paymentMethod: String = "" @Action fun submitOrder(): Continuation { // Process order with pre-calculated total val confirmation = OrderConfirmation( orderId = UUID.randomUUID().toString(), pizzas = pizzas, total = total, deliveryAddress = deliveryAddress ) // Workflow completes successfully return Continuation.end(confirmation) } } data class Pizza(val name: String, val price: BigDecimal) data class OrderConfirmation( val orderId: String, val pizzas: List, val total: BigDecimal, val deliveryAddress: String ) ``` -------------------------------- ### Test Pizza Ordering Workflow Logic (Kotlin) Source: https://github.com/lisegmbh/fluxflow/blob/develop/README.md Unit tests for the AddPizzaToCartStep and CheckoutStep classes, verifying the functionality of adding pizzas, transitioning to checkout, and submitting an order. ```kotlin class PizzaOrderWorkflowTest { @Test fun `addPizza should add pizza to cart and return to same step`() { // Arrange val initialStep = AddPizzaToCartStep() val pizza = Pizza("Margherita", BigDecimal("12.99")) // Act val result = initialStep.addPizza(pizza) // Assert assertThat(result.pizzas).hasSize(1) assertThat(result.pizzas[0]).isEqualTo(pizza) assertThat(result).isInstanceOf(AddPizzaToCartStep::class.java) } @Test fun `toCheckout should transition to checkout step with current pizzas`() { // Arrange val pizzas = mutableListOf( Pizza("Margherita", BigDecimal("12.99")), Pizza("Pepperoni", BigDecimal("14.99")) ) val step = AddPizzaToCartStep(pizzas) // Act val result = step.toCheckout() // Assert assertThat(result).isInstanceOf(CheckoutStep::class.java) assertThat(result.pizzas).hasSize(2) assertThat(result.total).isEqualTo(BigDecimal("27.98")) } @Test fun `submitOrder should create confirmation with pre-calculated total`() { // Arrange val pizzas = listOf( Pizza("Margherita", BigDecimal("12.99")), Pizza("Pepperoni", BigDecimal("14.99")) ) val checkoutStep = CheckoutStep(pizzas, BigDecimal("27.98")).apply { deliveryAddress = "123 Main St" paymentMethod = "Credit Card" } // Act val result = checkoutStep.submitOrder() // Assert assertThat(result.isCompleted).isTrue() val confirmation = result.completionData as OrderConfirmation assertThat(confirmation.pizzas).isEqualTo(pizzas) assertThat(confirmation.total).isEqualTo(BigDecimal("27.98")) assertThat(confirmation.deliveryAddress).isEqualTo("123 Main St") assertThat(confirmation.orderId).isNotBlank() } } ``` -------------------------------- ### Access Property Value Source: https://github.com/lisegmbh/fluxflow/blob/develop/docs/features/queries.md Use the 'get' operator for type-safe access to object properties. It supports single property access and chained property access for nested objects. ```kotlin query.where { get(User::name).isEqual("John") } ``` ```kotlin query.where { get(User::address).get(Address::city).isEqual("Berlin") } ``` -------------------------------- ### Action Parameter Injection vs. Constructor Injection Source: https://github.com/lisegmbh/fluxflow/blob/develop/docs/steps.md Demonstrates how injecting dependencies like MailService via constructor parameters can complicate upstream steps. This approach forces all preceding steps to also declare MailService as a dependency. ```kotlin @Service class MailService { // fun sendMail(receiver: String, message: String) {} } @Step class CompleteOrderStep( val customerMailAddress: String, private val mailService: MailService // ) { @Action fun completeOrder() { mailService.sendMail(customerMailAddress, "Thank you for your order") // } } @Step class ReviewShoppingCart( val customerMailAddress: String, private val mailService: MailService // ) { @Action fun proceedToCheckout(): CompleteOrderStep { return CompleteOrderStep( customerMailAddress, mailService // ) } } ``` -------------------------------- ### String Matching: startsWith Source: https://github.com/lisegmbh/fluxflow/blob/develop/docs/features/queries.md Tests if a string begins with a specified prefix. Case sensitivity can be controlled. Requires importing `ExpressionExtensions.Strings.startsWith`. ```kotlin query.where { get(User::email).startsWith("admin@") // With case sensitivity: get(User::email).startsWith("ADMIN@", ignoreCasing = false) } ``` -------------------------------- ### Assigning Step Data with StepDataService Source: https://github.com/lisegmbh/fluxflow/blob/develop/docs/steps.md Use StepDataService.setValue to update step data. The data must implement the ModifiableData interface. This example shows updating an Instant value. ```kotlin import de.lise.fluxflow.api.step.stateful.data.Data import de.lise.fluxflow.api.step.stateful.data.StepDataService import java.time.Instant class ModificationHistoryService(private val stepDataService: StepDataService) { fun assignLastModifiedDate(data: Data) { stepDataService.setValue(data, Instant.now()) } } ``` -------------------------------- ### Create a Submit Step with an Action Source: https://github.com/lisegmbh/fluxflow/blob/develop/docs/getting-started/getting-started.md Define a step with @Step and include an @Action method to control workflow transitions. This action implicitly returns the next step. ```kotlin package de.lise.fluxflow.examples.ideas.workflow import de.lise.fluxflow.stereotyped.step.Step import de.lise.fluxflow.stereotyped.step.action.Action import de.lise.fluxflow.stereotyped.step.data.Data @Step class SubmitIdeaStep { @Data var summary: String = "" @Data var author: String = "" @Data var description: String = "" @Action fun submit(): IdeaReviewStep { return IdeaReviewStep() } } ``` -------------------------------- ### Modifying Step Metadata at Runtime Source: https://github.com/lisegmbh/fluxflow/blob/develop/docs/steps.md Use StepService.setMetadata to dynamically change a step's metadata. Passing null removes the metadata. This example shows assigning and clearing a list of user IDs. ```kotlin import de.lise.fluxflow.api.step.Step import de.lise.fluxflow.api.step.StepService class UserTaskService( private val stepService: StepService ) { fun assignUsers(step: Step, userIds: List) { stepService.setMetadata(step, ASSIGNED_USERS_KEY, userIds) } fun clearAssignedUsers(step: Step) { stepService.removeMetadata(step, ASSIGNED_USERS_KEY) } private companion object { const val ASSIGNED_USERS_KEY = "assignedUsers" } } ``` -------------------------------- ### Define a Job with Parameters Source: https://github.com/lisegmbh/fluxflow/blob/develop/docs/jobs.md Declare public properties in your job class to accept parameters. These parameters can be used to dynamically customize job behavior and are passed into the constructor during job activation. ```kotlin @Job class SendReminderJob( val receiverAddress: String // ) { fun doSendMail() { // Do work here } } ``` -------------------------------- ### Access Map Value by Key with FluxFlow Source: https://github.com/lisegmbh/fluxflow/blob/develop/docs/features/queries.md Use the `get` extension to access values within a map using a constant or dynamic key. This is useful for filtering data based on nested map properties. ```kotlin query.where { get(User::attributes).get(const("role")).isEqual("admin") // Or with a dynamic key: get(User::attributes).get(other.get(Setting::key)).isEqual("value") } ``` -------------------------------- ### Pass Data to Next Step via Constructor Source: https://github.com/lisegmbh/fluxflow/blob/develop/docs/getting-started/getting-started.md In the @Action method, instantiate the next step, passing the current step's data as constructor arguments to transfer information. ```kotlin package de.lise.fluxflow.examples.ideas.workflow import de.lise.fluxflow.stereotyped.step.Step import de.lise.fluxflow.stereotyped.step.action.Action import de.lise.fluxflow.stereotyped.step.data.Data @Step class SubmitIdeaStep { @Data var summary: String = "" @Data var author: String = "" @Data var description: String = "" @Action fun submit(): IdeaReviewStep { return IdeaReviewStep( summary, author, description ) } } ``` -------------------------------- ### Define a Basic Workflow Model Source: https://github.com/lisegmbh/fluxflow/blob/develop/docs/workflow/model.md Create a plain Kotlin data class to serve as the workflow model. This class holds the state shared across the workflow's execution. ```kotlin import java.time.Instant enum class Status { Draft, Submitted, Approved, Rejected } data class VacationRequestWorkflow( // This will be the workflow model. val creationTime: Instant, var status: Status = Status.Draft ) ``` -------------------------------- ### Create a Workflow Step with Actions (Java) Source: https://github.com/lisegmbh/fluxflow/blob/develop/README.md Define a workflow step using Java annotations. Use `@Data` for step-specific state and `@Action` for transitions. Validation can be configured using `ValidationBehavior`. ```java // Java Example - Pizza Ordering Step @Step public class AddPizzaToCartStep { @Data @NotEmpty(message = "Cart cannot be empty. Please add at least one pizza before checkout.") private List pizzas = new ArrayList<>(); @Action(beforeExecutionValidation = ValidationBehavior.AllowInvalid) public AddPizzaToCartStep addPizza(Pizza pizza) { // Add pizza to cart pizzas.add(pizza); return new AddPizzaToCartStep(pizzas); } @Action public CheckoutStep toCheckout() { // Calculate total and proceed to checkout BigDecimal total = pizzas.stream() .map(Pizza::getPrice) .reduce(BigDecimal.ZERO, BigDecimal::add); return new CheckoutStep(pizzas, total); } } ``` -------------------------------- ### Schedule a Job Using JobContinuation Source: https://github.com/lisegmbh/fluxflow/blob/develop/docs/jobs.md Construct a JobContinuation using Continuation.job() to schedule a job for a specific time. The job instance is provided as the second parameter. If the scheduled time has passed, the job may execute immediately. ```kotlin @Job class SendMailJob( private val receiverAddress: String ) { fun execute() { // do actual work } } Continuation.job( // Instant.now().plus(Duration.ofMinutes(5)), // SendMailJob("receiver@example.com") // ) ``` -------------------------------- ### Step Definition with Simple Step Data Source: https://github.com/lisegmbh/fluxflow/blob/develop/docs/steps.md Add step data by declaring publicly accessible properties within the step definition class. The property name becomes the data's kind. ```kotlin class SubmitPizzaOrderStep { var mailAddress: String? = null } ``` -------------------------------- ### Specify Compatible Versions for a Step Source: https://github.com/lisegmbh/fluxflow/blob/develop/docs/features/versioning.md When a new version is introduced, you can list older, still compatible versions using the `compatibleVersions` property within the @Version annotation. This helps maintain backward compatibility. ```kotlin @Step @Version( "0.0.5", compatibleVersions = ["0.0.4", "0.0.3"] ) class EnterContactInformationStep { // [...] ``` -------------------------------- ### Workflow Fork Behaviors Source: https://github.com/lisegmbh/fluxflow/blob/develop/docs/steps.md Demonstrates explicit configuration of fork behaviors for workflows. Use ForkBehavior.Fork, ForkBehavior.Remove, or ForkBehavior.Replace to control how the workflow continues. ```kotlin fun submit(): Continuation<*> { val workflowModel = CustomerFeedback() val initialStep = Continuation.step(AskForFeedbackStep(workflowModel)) val defaultBehavior = Continuation.workflow(workflowModel, initialStep) val explicitFork = Continuation.workflow(workflowModel, initialStep, ForkBehavior.Fork) val explicitForkFromBuilder = Continuation.workflow(workflowModel, initialStep) .withForkBehavior(ForkBehavior.Fork) val explicitRemove = Continuation.workflow(workflowModel, initialStep, ForkBehavior.Remove) val explicitRemoveFromBuilder = Continuation.workflow(workflowModel, initialStep) .withForkBehavior(ForkBehavior.Remove) val exlicitReplace = Continuation.workflow(workflowModel, initialStep, ForkBehavior.Replace) val explicitReplaceFromBuilder = Continuation.workflow(workflowModel, initialStep) .withForkBehavior(ForkBehavior.Replace) return defaultBehavior } ``` -------------------------------- ### Applying Metadata to Constructor Properties in Kotlin Source: https://github.com/lisegmbh/fluxflow/blob/develop/docs/steps.md Illustrates applying metadata annotations to properties defined in a Kotlin constructor. Be cautious as annotations default to applying to constructor parameters. ```kotlin @Step class EnterContactDetails( @property:DisplayName("Given name") var firstname: String = "" ) ``` -------------------------------- ### Workflow Replacement with Scope Source: https://github.com/lisegmbh/fluxflow/blob/develop/docs/steps.md Illustrates replacing a workflow with a new one, including specifying which elements (Steps, Jobs, ContinuationRecords) should be replaced. This is useful for updating workflow definitions without emitting a WorkflowDeletedEvent. ```kotlin fun submit(): Continuation<*> { val workflowModel = CustomerFeedback() val initialStep = Continuation.step(AskForFeedbackStep(workflowModel)) val replaceWorkflow = Continuation.workflow( workflowModel, initialStep, ForkBehavior.Replace, setOf( WorkflowElement.Step, WorkflowElement.Job, WorkflowElement.ContinuationRecord, ) ) return replaceWorkflow } ``` -------------------------------- ### Rollback Continuation Source: https://github.com/lisegmbh/fluxflow/blob/develop/docs/steps.md Implement `Continuation.rollback()` to define a rollback mechanism. This attempts to reactivate the previous step in case of cancellation. Ensure a previous step exists to avoid exceptions. ```kotlin class ModifyBasketStep { // ... fun processToCheckout(): SubmitPizzaOrderStep { return SubmitPizzaOrderStep() // } } class SubmitPizzaOrderStep { fun submit() { // do actual checkout } fun cancel(): RollbackContinuation { return Continuation.rollback() // } } ``` -------------------------------- ### Importing Data with Listeners in Kotlin Source: https://github.com/lisegmbh/fluxflow/blob/develop/docs/steps.md Demonstrates how listeners declared on imported data types are retained and how additional listeners on the importing step are merged. Use when encapsulating related data and its event handling. ```kotlin data class ImportableWithListener( @Data val value: String ) { @DataListener("value") fun onValueChanged(oldValue: String?, newValue: String?) { /* ... */ } } @Step class ParentWithImportAndListeners( @Import val imported: ImportableWithListener ) { @DataListener("value") fun onImportedValueChanged(oldValue: String?, newValue: String?) { /* ... */ } } @Step class ParentWithPrefixedImportAndListener( @Import(prefix = "prefix", prefixStrategy = PrefixStrategy.CamelCase) val imported: ImportableWithListener ) { @DataListener("prefixValue") fun onPrefixedImportedValueChanged(oldValue: String?, newValue: String?) { /* ... */ } } ``` -------------------------------- ### Rollback with Status Preservation Source: https://github.com/lisegmbh/fluxflow/blob/develop/docs/steps.md Configure rollback continuations to preserve the status of the current step using `withStatusBehavior(StatusBehavior.Preserve)` or the `@Action(statusBehavior = ImplicitStatusBehavior.Preserve)` annotation. ```kotlin @Action(statusBehavior = ImplicitStatusBehavior.Preserve) fun cancel(): Continuation<*> { return Continuation.rollback() .withStatusBehavior(StatusBehavior.Preserve) } ``` -------------------------------- ### Continue Workflow with New Step (Kotlin) Source: https://github.com/lisegmbh/fluxflow/blob/develop/docs/steps.md Use explicit or implicit declarations to continue a workflow with a new step. Implicit continuation returns the next step definition directly, while explicit continuation uses `Continuation.step()`. ```kotlin class SubmitPizzaOrderStep { fun submitImplicit(): CompleteOrderStep { // return CompleteOrderStep() } fun submitExplicit(): Continuation { // return Continuation.step(CompleteOrderStep()) } } class CompleteOrderStep ``` -------------------------------- ### Complete Step Without New Operations (Kotlin) Source: https://github.com/lisegmbh/fluxflow/blob/develop/docs/steps.md Actions returning nothing or `Continuation.none()` will trigger no additional operations. This is useful for completing a step without proceeding further in the workflow. ```kotlin class CompleteOrderStep { fun completeImplicit() {} // fun completeExplicit(): Continuation<*> { // return Continuation.none() } } ``` -------------------------------- ### Sort Workflows by City and Invoiced Amount Source: https://github.com/lisegmbh/fluxflow/blob/develop/docs/features/queries.md Apply sequential sorting to workflows, first by city in ascending order, then by invoiced amount in descending order. ```kotlin data class PizzaOrder(val city: String, val invoicedAmount: Double) // Sort workflows by city (ascending) then by invoiced amount (descending) val query = FlowQuery.of() .sort { Sorting.asc(get(WorkflowQueryable::model).get(PizzaOrder::city)) } .sort { Sorting.desc(get(WorkflowQueryable::model).get(PizzaOrder::invoicedAmount)) } ``` -------------------------------- ### Define a Basic Job Source: https://github.com/lisegmbh/fluxflow/blob/develop/docs/jobs.md Implement a new class annotated with @Job to define a job's behavior. The method annotated with @JobPayload will be executed when the job is scheduled. ```kotlin @Job class SendReminderJob { fun doSendMail() { // Do work here } } ``` -------------------------------- ### Define a Basic Step Action Source: https://github.com/lisegmbh/fluxflow/blob/develop/docs/steps.md A basic step action is a public, non-abstract, non-static function that either has no parameters (if not annotated with @Action) or only injectable parameters (if annotated with @Action). ```kotlin class SubmitPizzaOrderStep { fun submit() { // send notification to the restaurant } } ``` -------------------------------- ### Configure Spring Boot Application with FluxFlow Source: https://github.com/lisegmbh/fluxflow/blob/develop/README.md Enable FluxFlow integration in your Spring Boot application by adding the `@EnableFluxFlow` annotation to your main application class. ```kotlin @SpringBootApplication @EnableFluxFlow class MyApplication fun main(args: Array) { runApplication(*args) } ``` -------------------------------- ### Versioning Steps with @Version Source: https://context7.com/lisegmbh/fluxflow/llms.txt Annotate steps with @Version to manage compatibility across different code versions. Specify compatible versions to allow older steps to be loaded. The StepDefinition provides runtime access to the step's version. ```kotlin import de.lise.fluxflow.stereotyped.step.Step import de.lise.fluxflow.stereotyped.versioning.Version import de.lise.fluxflow.api.step.StepDefinition @Step @Version( "1.2.0", compatibleVersions = ["1.1.0", "1.0.0"] // older versions still loadable without fallback ) class CheckoutStep( @de.lise.fluxflow.stereotyped.step.data.Data var cartId: String = "", @de.lise.fluxflow.stereotyped.step.data.Data var promoCode: String? = null ) // Reading a step's persisted version at runtime: fun logVersion(stepDef: StepDefinition) { println("Step '${stepDef.kind}' version: ${stepDef.version}") } ``` -------------------------------- ### Adding Custom Metadata Annotation in Kotlin Source: https://github.com/lisegmbh/fluxflow/blob/develop/docs/steps.md Shows how to create a custom metadata annotation like `@DisplayName` and apply it to a step property. Use to attach static, descriptive information to step data fields. ```kotlin import de.lise.fluxflow.stereotyped.metadata.Metadata import de.lise.fluxflow.stereotyped.step.Step @Metadata("displayName") annotation class DisplayName(val value: String) @Step class EnterContactDetails { @DisplayName("Given name") var firstname: String = "" } ``` -------------------------------- ### Attach Metadata to Steps with @Metadata and StepService Source: https://context7.com/lisegmbh/fluxflow/llms.txt Use `@Metadata` annotations to define custom metadata keys and apply them statically to step definitions. At runtime, `StepService.setMetadata` and `StepService.removeMetadata` can be used to manage metadata on step instances. ```kotlin import de.lise.fluxflow.api.step.Step import de.lise.fluxflow.api.step.StepService import de.lise.fluxflow.stereotyped.metadata.Metadata import de.lise.fluxflow.stereotyped.step.Step as StepAnnotation import org.springframework.stereotype.Service // 1. Define a custom metadata annotation @Metadata("displayName") annotation class DisplayName(val value: String) @Metadata("category") annotation class Category(val value: String) // 2. Apply it statically to a step definition @StepAnnotation @DisplayName("Complete Order") @Category("checkout") class CompleteOrderStep { // ... } // 3. Set/clear metadata at runtime @Service class TaskAssignmentService(private val stepService: StepService) { fun assign(step: Step, userIds: List) { stepService.setMetadata(step, "assignedUsers", userIds) // Retrieve: step.definition.metadata["assignedUsers"] } fun unassign(step: Step) { stepService.removeMetadata(step, "assignedUsers") // Or: stepService.setMetadata(step, "assignedUsers", null) } } ``` -------------------------------- ### Model Listener with Workflow API and DI Source: https://github.com/lisegmbh/fluxflow/blob/develop/docs/workflow/model.md Access the current workflow instance and services from dependency injection by declaring typed parameters in your @ModelListener function. ```kotlin @Service class MailService { fun sendMail() { /* Implement */ } } data class VacationRequestWorkflow( val creationTime: Instant, var status: Status = Status.Draft ) { @ModelListener fun anythingChanged( // receives the currently executing workflow currentWorkflow: Workflow, // receives the service from dependency injection mailService: MailService ) { // react to changes } } ``` -------------------------------- ### Combine Filtering, Sorting, and Pagination in FlowQuery Source: https://github.com/lisegmbh/fluxflow/blob/develop/docs/features/queries.md Chain multiple FlowQuery operations to build a complex query that filters results, sorts them, and applies pagination. ```kotlin data class PizzaOrder(val city: String, val invoicedAmount: Double) val query = FlowQuery.of() .where { get(WorkflowQueryable::model) .get(PizzaOrder::city) .isEqual("Cologne") } .sort { Sorting.asc(get(WorkflowQueryable::model).get(PizzaOrder::invoicedAmount)) } .paged(pageIndex = 0, pageSize = 20) ``` -------------------------------- ### Paginate Workflow Results Source: https://github.com/lisegmbh/fluxflow/blob/develop/docs/features/queries.md Retrieve paginated results from a workflow query, specifying the page index and the number of items per page. ```kotlin data class PizzaOrder(val city: String) // Get the first page with 50 items per page val query = FlowQuery.of() .paged(pageIndex = 0, pageSize = 50) // Or use a PaginationRequest object val query = FlowQuery.of() .paged(PaginationRequest(pageIndex = 0, pageSize = 50)) ``` -------------------------------- ### Explicitly Continue Workflow with a Step Source: https://github.com/lisegmbh/fluxflow/blob/develop/docs/getting-started/getting-started.md Use Continuation.step() within an @Action method to explicitly define the next step in the workflow. ```kotlin package de.lise.fluxflow.examples.ideas.workflow import de.lise.fluxflow.api.continuation.Continuation import de.lise.fluxflow.stereotyped.step.Step import de.lise.fluxflow.stereotyped.step.action.Action import de.lise.fluxflow.stereotyped.step.data.Data @Step class SubmitIdeaStep { @Data var summary: String = "" @Data var author: String = "" @Data var description: String = "" @Action fun submit(): Continuation { return Continuation.step(IdeaReviewStep()) } } ``` -------------------------------- ### FluxFlow Automation Functions (@OnCreated, @OnCompleted) Source: https://context7.com/lisegmbh/fluxflow/llms.txt Illustrates automation functions that execute automatically on step lifecycle events. Includes scheduling a job on step creation and sending a notification on step completion. ```kotlin import de.lise.fluxflow.api.continuation.Continuation import de.lise.fluxflow.stereotyped.step.Step import de.lise.fluxflow.stereotyped.step.action.Action import de.lise.fluxflow.stereotyped.step.automation.trigger.OnCreated import de.lise.fluxflow.stereotyped.step.automation.trigger.OnCompleted import java.time.Duration import java.time.Instant @Step class ReviewRequestStep { // Runs automatically when this step is created; schedules a cancellation job @OnCreated fun scheduleTimeout(): Continuation<*> = Continuation.job( Instant.now().plus(Duration.ofDays(3)), TimeoutCancelJob() ) // Runs automatically when this step completes @OnCompleted fun sendCompletionNotification(mailService: MailService) { mailService.sendNotification("Review step completed") } @Action fun approve(): ApprovedStep = ApprovedStep() @Action fun reject(): RejectedStep = RejectedStep() } @Job class TimeoutCancelJob { fun execute() { println("Review timed out; canceling request") } } ``` -------------------------------- ### Define Workflow Step with @Step and @Data Annotations Source: https://context7.com/lisegmbh/fluxflow/llms.txt Define a workflow step using a class annotated with `@Step`. Properties marked with `@Data` become persistent state, and public methods become invocable actions. ```kotlin import de.lise.fluxflow.stereotyped.step.Step import de.lise.fluxflow.stereotyped.step.data.Data import de.lise.fluxflow.stereotyped.step.action.Action import jakarta.validation.constraints.NotBlank @Step class SubmitIdeaStep { @Data @get:NotBlank var summary: String = "" @Data var author: String = "" @Data var description: String = "" @Action fun submit(): IdeaReviewStep { // Returning a step instance implicitly transitions the workflow to that step return IdeaReviewStep(summary, author, description) } } @Step class IdeaReviewStep( @Data val summary: String, @Data val author: String, @Data val description: String ) { @Data var feedback: String = "" @Action fun approve(): IdeaApprovedStep = IdeaApprovedStep(summary, author) @Action fun reject(): IdeaRejectedStep = IdeaRejectedStep(summary, feedback) } data class IdeaApprovedStep(@Data val summary: String, @Data val author: String) data class IdeaRejectedStep(@Data val summary: String, @Data val reason: String) ``` -------------------------------- ### Define a Job with Parameters and Injected Services Source: https://context7.com/lisegmbh/fluxflow/llms.txt Define a job using the `@Job` annotation. Use constructor parameters for job-specific data and Spring services for external dependencies. Jobs can be rescheduled by returning a new `Continuation.job`. ```kotlin import de.lise.fluxflow.api.continuation.Continuation import de.lise.fluxflow.api.job.Job import de.lise.fluxflow.api.workflow.Workflow import de.lise.fluxflow.stereotyped.job.Job as JobAnnotation import de.lise.fluxflow.stereotyped.step.Step import de.lise.fluxflow.stereotyped.step.action.Action import de.lise.fluxflow.stereotyped.step.action.ImplicitStatusBehavior import java.time.Duration import java.time.Instant import java.time.temporal.ChronoUnit @JobAnnotation class SendReminderJob( val receiverAddress: String, // persisted job parameter (public property) private val mailService: MailService // injected from Spring context (private = not a parameter) ) { fun sendReminder( job: Job, // injected: currently executing job workflow: Workflow // injected: owning workflow ): Continuation<*> { println("Executing job ${job.identifier} for workflow ${workflow.id}") mailService.send(receiverAddress, "Reminder: please complete your review") workflow.model.status // can read/modify workflow model; changes are committed on success // Reschedule for another 24h later (recurring pattern) return Continuation.job( Instant.now().plus(Duration.ofHours(24)), SendReminderJob(receiverAddress, mailService) ) } } // Scheduling a job from a step action using a CancellationKey @Step class WaitForApprovalStep { @Action(statusBehavior = ImplicitStatusBehavior.Preserve) fun snoozeReminder(): Continuation<*> = Continuation.job( Instant.now().plus(10, ChronoUnit.MINUTES), SendReminderJob("approver@example.com", /* injected */ TODO()), CancellationKey("reminder-alarm") // replaces any existing job with this key ) @Action(statusBehavior = ImplicitStatusBehavior.Preserve) fun silenceReminder(): Continuation<*> = Continuation.cancelJobs(CancellationKey("reminder-alarm")) @Action fun approve(): ApprovedStep = ApprovedStep() } ```