Skip to Content

Flow Starter

Flow Concept

Flow is a more generalized concept for business processes.

Relationship: Flow > BP = Workflow > BPMN

Expression Engine is mainly used to solve data computation problems, implementing a calculation engine such as QLExpress. BPMN scenarios are better suited to workflow or rule engines like Flowable. For example, Flowable integrates JUEL to support simple expression parsing. DMN rule engines are better for true/false decision scenarios, for example, the sequenceFlow routing conditions in BP. Drools focuses more on rule configuration and management.

Example scenarios:

  • Import: better suited to BPMN. Each action is a data update node; actions have an order but can be processed asynchronously.
  • Approval: BPMN + DMN. BPMN handles the approval process and path. DMN handles complex approval routing. DQL solves complex approver queries.

flow-starter is Softa’s metadata-driven flow engine starter. It covers:

  • design-time graph documents for a visual editor
  • compile-time validation and transformation into runtime bundles
  • published bundle revisions by designId
  • runtime execution for process, validation, and compute flows
  • persistent projections for instances, approval tasks, approval records, delegations, CC rules, and trigger events
  • optional Pulsar-based trigger and async-task integration

This page documents the current backend contract implemented in flow-starter.

Current Model

Design, Publish, Runtime

LayerMain typesNotes
Design timeDesignFlowDefinition, FlowGraphDocument, FlowGraphNode, FlowGraphEdgeEditor-facing graph DTOs
Publish timeFlowCompiler, CompiledFlowDefinition, FlowPublishServiceCompile, validate, publish revisions, rollback
RuntimeFlowRuntimeEngine, FlowExecutionStateExecute published bundles, suspend/resume, persist projections

Flow Identity And Revisions

  • FlowDesign is the draft working copy.
  • FlowBundle is the compiled published snapshot.
  • Published revisions are keyed by designId, not by flowCode.
  • FlowStartRequest resolves bundles in this order:
    1. bundleId — pin to an exact published revision
    2. designId — start the current active revision
  • To start a specific historical revision, look up its bundleId via the revisions listing endpoint and pass that as bundleId.
  • FlowBundle.designJson keeps the design snapshot used at publish time so the editor can restore a historical canvas.

Flow Scenarios

The module now uses a single FlowScenario enum instead of the old FlowKind + FlowPurpose split.

ScenarioMeaning
PROCESSStateful runtime flow with persisted instances; covers both approval-style and automation-style flows
VALIDATIONStateless synchronous validation flow — evaluated transiently: no instance row, no trace, no event log
COMPUTEStateless synchronous compute/onchange flow — evaluated transiently, same zero-footprint rules

The scenario decides the execution strategy: PROCESS flows go through FlowRuntimeEngine.start (persisted, resumable); VALIDATION / COMPUTE flows go through evaluate — the same traversal with zero flow footprint. Business writes made by task nodes still join the caller’s transaction, so a failed evaluation rolls them back.

Node Types

Current FlowNodeType values:

  • Control: Start, End, Timer
  • Routing: InclusiveGateway, ParallelFork, ParallelJoin
  • Human: Approval, HumanTask*
  • Task: Script, CreateRecord, GetRecord, UpdateRecord, DeleteRecord, QueryRecords, ValidateData, Transform, CallService, CallWebhook, SendEmail, SendSms, SendInboxNotification, QueryAI, AsyncTask, GenerateFile
  • Subflow: Subflow
  • Data: ForEach*, ReturnValue

* HumanTask and ForEach are defined but not yet runtime-supported: they are omitted from the node palette and rejected at compile time (UNSUPPORTED_NODE_TYPE), so a flow containing them cannot publish.

Node-behavior invariants:

  • Subflow is synchronous-only: the orchestrator fails the parent if a child enters a waiting state (WAITING_APPROVAL / TIMER / ASYNC), and recursive subflow invocation is rejected. Model human steps in the parent flow, not a child.
  • ParallelFork branches execute serially (deterministic order), not wall-clock-parallel — the gateway models routing topology, not concurrency.

Runtime Statuses

Defined FlowExecutionStatus values:

  • Pending
  • Running
  • Waiting — parked on one or more waits (a pending approval and/or timer / async callback). The specific reasons live in the instance’s pending-approval list and wait-token list, so a single status covers parallel branches suspended for different reasons at once.
  • Rejected
  • Returned
  • Withdrawn
  • Cancelled
  • Completed
  • Failed

API Overview

Host applications embedding the engine in-process should integrate through the FlowClient facade (io.softa.starter.flow.api) rather than looping back over REST — actor / initiator / tenant identity is always resolved server-side from the context, never passed as call parameters. See the in-module integration guide under starters/flow-starter/docs/integration-guide.md in the Softa source repository.

Design APIs (flow editor)

Base path: /flow/designs — the dedicated surface for the graphical editor (the full editor API contract lives in the Softa source repository under starters/flow-starter/docs/frontend-editor-api.md). The generic model API at /FlowDesign/** remains available as the platform data plane and is intentionally not intercepted.

MethodPathDescription
POST/flow/designsCreate a draft
GET/flow/designs?keyword=&scenario=&pageNumber=&pageSize=Paged draft list (canvas excluded)
GET/flow/designs/{id}Load one draft (canvas + optimistic-lock version)
POST/flow/designs/{id}/saveAuto-save the canvas; stale versions are rejected
POST/flow/designs/{id}/deleteDelete a draft if it has never been published
POST/flow/designs/{id}/duplicateCopy a draft under a fresh flow code
POST/flow/designs/validateValidate the posted document; diagnostics returned as a success payload
POST/flow/designs/{id}/publishPublish the saved draft as a new active revision
POST/flow/designs/{id}/restore?bundleId=Restore the draft canvas from a historical bundle snapshot
GET/flow/designs/{id}/statusPublish status: revision badge + dirty flag
GET/flow/designs/{id}/availableVariables?nodeId=Variables visible to a node (expression autocomplete)
POST/flow/designs/{id}/debugRunCompile and run the current draft as a debug bundle (not a sandbox)
GET/flow/designs/{id}/debugRunsDebug-run history, newest first

Bundle APIs (published revisions)

Base path: /flow/bundles

MethodPathDescription
GET/flow/bundles?designId=Revision list of one design (or every design’s active bundle without designId)
GET/flow/bundles/{bundleId}?include=designOne revision’s summary, optionally with the canvas-complete design snapshot
POST/flow/bundles/{bundleId}/activateMake this revision the design’s effective one (rollback = roll-forward)

Node Descriptor APIs

Base path: /flow/nodeDescriptors

MethodPathDescription
GET/flow/nodeDescriptorsList all node descriptors for the editor palette
GET/flow/nodeDescriptors?scenario=PROCESSFilter descriptors by FlowScenario

Runtime APIs

Base path: /flow/runtime

MethodPathDescription
POST/flow/runtime/instances/startStart a flow instance
GET/flow/runtime/instances/{instanceId}?includeTrace=Get one runtime instance (trace excluded by default)
GET/flow/runtime/instances/{instanceId}/overlayPer-node run state for canvas painting
GET/flow/runtime/instances/{instanceId}/trace?sinceSequence=Incremental trace rows for polling
POST/flow/runtime/instances/searchPaged summaries of the caller’s own instances — the initiator filter is always the authenticated user (filters: flowCode/designId/status/model/row/createdFrom/createdTo)
POST/flow/runtime/instances/approveApprove a pending approval
POST/flow/runtime/instances/rejectReject a pending approval
POST/flow/runtime/instances/transferTransfer a pending approval task
POST/flow/runtime/instances/delegateDelegate a pending approval task
POST/flow/runtime/instances/add-sign-beforeInsert a prerequisite signer
POST/flow/runtime/instances/add-sign-afterInsert a follow-up signer
POST/flow/runtime/instances/ccSend a CC notification
POST/flow/runtime/instances/cc/readMark a CC as read
POST/flow/runtime/instances/returnReturn a pending approval
POST/flow/runtime/instances/resubmitResubmit a returned instance
POST/flow/runtime/instances/withdrawWithdraw a flow instance
POST/flow/runtime/instances/urgeUrge pending approvers
POST/flow/runtime/instances/commentAdd a comment audit entry
GET/flow/runtime/instances/{instanceId}/nodes/{nodeId}/formPermissionsGet field-level form permissions
POST/flow/runtime/triggerFire a trigger event and start matching flows
POST/flow/runtime/onchangeRun COMPUTE flows transiently and return variable diffs
POST/flow/runtime/validateRun VALIDATION flows transiently against candidate row data; returns each flow’s declared outputs keyed by flow code

Engine-internal resume callbacks live on /internal/flow/runtime/instances/{resumeAsync|resumeTimer} (INTERNAL identity scope) — they are not part of the user-facing surface.

actorId / initiatorId / tenantId are never sent by clients: every action request stamps them server-side from the login context (@JsonProperty(READ_ONLY)), and all query endpoints resolve the current actor from the context.

Approval Projection APIs

Approval Tasks

Base path: /flow/approvalTasks

MethodPathDescription
GET/flow/approvalTasks/countsInbox badge counts for the current actor ({pendingApprovals, unreadCc} — same filter definitions as the paged /pending and /cc?read=false queries)
GET/flow/approvalTasks/pending?flowCode=&instanceId=&nodeId=&pageNumber=&pageSize=Pending tasks for the current actor (paged: returns Page)
GET/flow/approvalTasks/completed?flowCode=&instanceId=&nodeId=&pageNumber=&pageSize=Completed tasks for the current actor (paged)
GET/flow/approvalTasks/cc?read=&flowCode=&instanceId=&nodeId=&pageNumber=&pageSize=CC tasks for the current actor (paged; read=false unread, read=true acknowledged, omit read for both)
GET/flow/approvalTasks/inbox?flowCode=&instanceId=&nodeId=&includeCompletedApprovals=&pageNumber=&pageSize=Unified inbox view (pending approvals + CC; optional completed approvals)
GET/flow/approvalTasks/instance/{instanceId}All tasks for one runtime instance (participants / initiator only)

Approval Records

Base path: /flow/approvalRecords

MethodPathDescription
GET/flow/approvalRecords/instance/{instanceId}Approval history by runtime instance
GET/flow/approvalRecords/historyActor-scoped approval history
GET/flow/approvalRecords/cc/sentSender-side CC history

CC Config

Base path: /flow/ccConfigs

MethodPathDescription
POST/flow/ccConfigsCreate a CC rule
GET/flow/ccConfigs?flowCode=List CC rules by flowCode
POST/flow/ccConfigs/{id}/deactivateDeactivate a CC rule

Delegation

Base path: /flow/delegations

MethodPathDescription
POST/flow/delegationsCreate a delegation rule
GET/flow/delegations/myDelegations created by the caller (delegator resolved from the login context)
GET/flow/delegations/to-meActive delegations assigned to the caller (delegate resolved from the login context)
POST/flow/delegations/{id}/cancelCancel a delegation rule

Monitor

Base path: /flow/monitor — operator-facing; the search endpoint requires the system admin role (@RequireRole(SystemRoleAdmin), fail-closed when the host app wires no role provider). A caller holding that role also bypasses the participant scoping on the instance detail / overlay / trace / approval-history reads.

MethodPathDescription
GET/flow/monitor/healthFlow runtime health snapshot (per-status instance counts, overdue timers)
POST/flow/monitor/instances/searchCross-initiator paged instance summaries; honors the request’s initiatorId filter when present

Event Logs

Base path: /flow/events

MethodPathDescription
GET/flow/events?flowCode=&sourceModel=&sourceRowId=&instanceId=&success=&eventTimeFrom=&eventTimeTo=&pageNumber=&pageSize=Paged event log, newest first (filters combine with AND; list rows exclude the parameters payload)
GET/flow/events/{id}Single event including the raw trigger-parameters payload

Approval Model

Approval And Reject Thresholds

  • Approval modes: ANY_ONE, UNANIMOUS, MIN_COUNT, PERCENTAGE
  • Reject modes: ANY_ONE, UNANIMOUS, MIN_COUNT, PERCENTAGE
  • One actor can only cast one effective vote per cycle.
  • Compile-time validators reject impossible threshold combinations.

Approver Resolution

Approval nodes support:

  • static approvers via config.approvers
  • dynamic approvers via config.approverSource
  • empty-approver strategies via config.emptyApproverStrategy

Supported dynamic source types in the current default resolution path:

  • VariableList
  • Expression
  • InitiatorManager
  • Role

Important integration note:

  • OrganizationService is defined as an SPI (MetadataOrganizationService is the default when wired). The default resolver path also supports variable-based sources via ApproverResolutionService.
  • If you need real org-tree lookup, provide a custom OrganizationService bean or populate resolution variables such as initiatorManagerId and roleApprovers before the flow reaches the approval node.

Approver dedup: the default policy is GLOBAL — a user who already approved an earlier node is auto-passed on later nodes (recorded as AUTO_APPROVE). Residual risk: a mid-flow form edit is still auto-endorsed by that earlier approval; set the policy to CONTIGUOUS or NONE on flows where a re-look matters.

Additional Approval Actions

  • transfer
  • delegate
  • add-sign-before
  • add-sign-after
  • cc
  • read-cc
  • return
  • resubmit
  • withdraw
  • urge
  • comment

Timeout Handling

Approval timeout strategies:

  • REMIND
  • AUTO_APPROVE
  • AUTO_REJECT
  • ESCALATE

ApprovalTimeoutScheduler handles reminders, auto actions, and escalation against persisted pending approvals.

Task Execution Model

Executor-backed task nodes use TaskNodeConfig. The node FlowNodeType selects the executor — there is no separate executor field in config.

{ "input": { "to": ["{{ applicantEmail }}"], "subject": "Order {{ orderId }} confirmed", "htmlBody": "<h1>Thank you for your order!</h1>" }, "outputVariable": "taskResult" }
  • input supports {{ expr }} interpolation; resolution is owned by the executor (type-aware for data executors, recursive interpolation for free-shape payloads)
  • options is executor-specific
  • outputVariable, when present, stores the raw executor result under one variable name

Every task node type has a typed input config DTO registered in TaskConfigTypes: the compiler parses config.input into the DTO at publish time and rejects missing required keys (MISSING_REQUIRED_INPUT), so a misconfigured node fails at publish instead of mid-execution. DefaultTaskExecutorRegistry asserts at boot that every registered executor’s node type has such an entry.

Bundled Task Executors

Node typeExecutor beanNotes
CREATE_RECORDCreateDataTaskExecutorBuilt in
GET_RECORDGetDataTaskExecutorBuilt in
UPDATE_RECORDUpdateDataTaskExecutorBuilt in
DELETE_RECORDDeleteDataTaskExecutorBuilt in
QUERY_RECORDSQueryRecordsTaskExecutorBuilt in; opt-out via flow.task.builtin.query-records.enabled=false to register a custom executor
VALIDATE_DATAValidateDataTaskExecutorBuilt in
TRANSFORMExtractTransformTaskExecutorBuilt in
CALL_WEBHOOKWebHookTaskExecutorBuilt in
CALL_SERVICECallServiceTaskExecutorDisabled by default. Opt-in via flow.task.builtin.call-service.enabled=true; a non-empty flow.task.call-service.allow-list of permitted bean-name prefixes is then mandatory
SEND_EMAILSendEmailTaskExecutorRegistered only when MessageService is available
SEND_SMSSendSmsTaskExecutorRegistered only when MessageService is available
SEND_INBOX_NOTIFICATIONSendInboxNotificationTaskExecutorRegistered only when MessageService is available
QUERY_AIQueryAiTaskExecutorRegistered only when AiRobotService is available
ASYNC_TASKAsyncTaskExecutorRegistered only when FlowAsyncTaskProducer is available
GENERATE_FILEGenerateFileTaskExecutorRegistered only when DocumentTemplateService (file-starter) is available

Current module notes:

  • CALL_SERVICE ships a bundled executor but is disabled by default: it invokes an arbitrary Spring bean method by name, so it must be explicitly enabled and given a non-empty bean-name allow-list.

Trigger And Messaging Integration

Supported trigger sources (TriggerSource sealed sub-types):

  • EntityChange
  • Api
  • Cron
  • Subflow
  • FieldChange (COMPUTE scenario only)

Pulsar-connected components are conditionally registered:

PropertyComponentPurpose
mq.topics.flow-event.topicFlowEventProducer, FlowEventConsumerAsync trigger fire and consume
mq.topics.flow-async-task.topicFlowAsyncTaskProducer, FlowAsyncTaskConsumerAsync task dispatch and callback resume
mq.topics.change-log.topicChangeLogFlowConsumerEntity change driven flow triggers
mq.topics.cron-task.topicCronTaskFlowConsumerCron-driven flow triggers
mq.topics.flow-timer.topicFlowTimerConsumerTimer wake-up consume side

Important timer note:

  • The module includes the timer resume consumer.
  • A scheduler or producer that publishes FlowTimerMessage still has to be provided by the host application or another integration module.

Persistence Model

EntityDescription
FlowDesignDraft working copy
FlowBundlePublished compiled bundle
FlowInstancePersisted runtime instance state (trace and approval history are NOT embedded — see the two dedicated tables)
FlowExecutionTraceAppend-only execution trace rows (the in-memory state keeps only the current attempt’s delta)
FlowApprovalTaskPending/completed/CC task projections
FlowApprovalRecordApproval action audit ledger — the authoritative approval history, flushed by the instance store in the same transaction as the instance row
FlowCcConfigAutomatic CC rules
FlowDelegationDelegation rules
FlowEventTrigger event logs
FlowDebugHistoryDebug snapshots
FlowParallelBranchParallel branch tracking

Primary storage adapters in this module:

  • OrmFlowBundleRegistry
  • OrmFlowInstanceStore

Node Descriptor Contract

The editor palette is built from FlowNodeDescriptor records exposed through /flow/nodeDescriptors.

  • structural descriptors come from BuiltinNodeDescriptorProvider
  • executor-backed task descriptors come from TaskExecutorDescriptorProvider
  • descriptors include label, icon, sort order, config schema, default config, and allowed scenarios

TaskExecutorDescriptorProvider only exposes task nodes for executors that are actually registered as Spring beans, so conditional executors such as QUERY_AI and ASYNC_TASK only appear when their dependencies are present.

Frontend Graph Compatibility

Design graph DTOs remain aligned with xyflow/react style structures:

xyflow/reactflow-starter
Node.idFlowGraphNode.id
Node.typeFlowGraphNode.type
Node.positionFlowGraphNode.position
Node.width / heightFlowGraphNode.width / height
Node.dataFlowGraphNode.data
Edge.idFlowGraphEdge.id
Edge.source / targetFlowGraphEdge.source / target
Edge.sourceHandle / targetHandleFlowGraphEdge.sourceHandle / targetHandle
Edge.typeFlowGraphEdge.type
Edge.labelFlowGraphEdge.label
Edge.dataFlowGraphEdge.data
Viewport {x, y, zoom}FlowGraphViewport {x, y, zoom}

Host Application Integration Checklist

Before enabling flow-starter in a real app, verify:

  • add flow-starter to the application classpath
  • create and expose FlowDesign drafts through the generic model APIs
  • provide a real ApprovalNotificationService if you need email, SMS, IM, or in-app notifications
  • provide a custom OrganizationService or populate approver resolution variables if you need manager/role/department based approvers
  • enable Pulsar topics if using triggers or async tasks
  • provide timer scheduling/publishing if using TIMER nodes
  • enable CALL_SERVICE explicitly (plus its bean-name allow-list) if flows need to invoke Spring beans; all other task node types ship working executors, conditionally registered on their capability beans
Last updated on