Create Workflow
Create properly structured Square Workflow classes following library conventions.
Quick Start
To create a new workflow, gather:
- •Workflow name (e.g.,
Login) - •Stateful or Stateless — does it need internal state?
- •Props — input data from parent (
Unitif none) - •Output — events emitted to parent (
Nothingif none) - •Rendering — UI model returned each render pass (typically a
Screendata class)
Step 1: Choose Workflow Type
StatefulWorkflow — use when you need internal state:
- •Form data, loading states, error states
- •Multi-step flows or navigation
- •State that changes in response to events
StatelessWorkflow — use when rendering is a direct function of props:
- •Simple pass-through or composition of child workflows
- •No internal state needed
- •Rendering derived entirely from props and child renderings
Step 2: Define Types
Props (input from parent)
data class MyWorkflowProps(val userId: String) // Or use Unit if no props needed
State (StatefulWorkflow only)
State should use any Kotlin types - often a data class. If your state needs to be persisted
to disk via a Snapshot it needs to be able to be converted to a ByteString.
// Simple state
data class MyState(val name: String, val isLoading: Boolean)
// Sealed state for distinct modes
sealed interface MyState {
data object Loading : MyState
data class Loaded(val data: String) : MyState
data class Error(val message: String) : MyState
}
Output (events to parent)
// Multiple output types
sealed interface MyOutput {
data object Completed : MyOutput
data class Selected(val item: Item) : MyOutput
}
// Or use Nothing if no output is ever emitted
Rendering (view model)
Create in a separate file named [Feature]Screen.kt:
data class MyScreen( val title: String, val isLoading: Boolean, val onAction: () -> Unit, val onItemSelected: (Item) -> Unit ) : Screen
Step 3: Create Workflow Class
StatefulWorkflow — Object Pattern (no dependencies)
Use when the workflow has no injected dependencies:
object MyWorkflow : StatefulWorkflow<MyProps, MyState, MyOutput, MyScreen>() {
override fun initialState(props: MyProps, snapshot: Snapshot?): MyState =
MyState.Loading
override fun render(
renderProps: MyProps,
renderState: MyState,
context: RenderContext
): MyScreen {
return MyScreen(
title = renderProps.title,
isLoading = renderState is MyState.Loading,
onAction = context.eventHandler("onAction") {
setOutput(MyOutput.Completed)
},
onItemSelected = context.eventHandler("onItemSelected") { item: Item ->
state = MyState.Loaded(item.name)
}
)
}
override fun snapshotState(state: MyState): Snapshot? = null
}
StatefulWorkflow — Class Pattern (with dependencies)
Use when the workflow needs injected dependencies:
class MyWorkflow(
private val repository: DataRepository,
) : StatefulWorkflow<MyProps, MyState, MyOutput, MyScreen>() {
override fun initialState(props: MyProps, snapshot: Snapshot?): MyState =
MyState.Loading
override fun render(
renderProps: MyProps,
renderState: MyState,
context: RenderContext
): MyScreen {
// Run a worker for async data loading
context.runningWorker(
Worker.from { repository.fetchData(renderProps.id) },
key = "fetchData-${renderProps.id}"
) { result ->
action("dataLoaded") {
state = MyState.Loaded(result)
}
}
return MyScreen(
title = renderProps.title,
isLoading = renderState is MyState.Loading,
onAction = context.eventHandler("onAction") {
setOutput(MyOutput.Completed)
}
)
}
override fun snapshotState(state: MyState): Snapshot? = null
}
StatelessWorkflow
object MyWorkflow : StatelessWorkflow<MyProps, MyOutput, MyScreen>() {
override fun render(
renderProps: MyProps,
context: RenderContext
): MyScreen {
return MyScreen(
title = renderProps.title,
onAction = context.eventHandler("onAction") { action: String ->
setOutput(MyOutput.ActionSelected(action))
}
)
}
}
Factory Functions (inline definitions)
For simple workflows that don't need a named class:
// Stateful
val counterWorkflow = Workflow.stateful<Unit, Int, Nothing, Int>(
initialState = 0,
render = { state ->
state
}
)
// Stateless
val greetingWorkflow = Workflow.stateless<String, Nothing, String> { name ->
"Hello, $name!"
}
Workers (Async Operations)
Use Workers for async work. Never perform side effects directly in render().
// Single-value worker from a suspend function
val worker = Worker.from { api.fetchUser(userId) }
// Multi-value worker from a Flow
val worker = repository.observeUpdates().asWorker()
// Custom worker with doesSameWorkAs for deduplication
class FetchWorker(private val id: String) : Worker<Data> {
override fun run(): Flow<Data> = flow { emit(api.fetch(id)) }
override fun doesSameWorkAs(otherWorker: Worker<*>): Boolean =
otherWorker is FetchWorker && otherWorker.id == id
}
// Using a worker in render():
context.runningWorker(worker, key = "fetch") { result ->
action("fetched") { state = MyState.Loaded(result) }
}
Child Workflows
// Render a child and handle its output
val childScreen = context.renderChild(
child = ChildWorkflow,
props = ChildProps(renderProps.itemId)
) { childOutput ->
action("childOutput") {
when (childOutput) {
is ChildOutput.Done -> setOutput(MyOutput.Completed)
is ChildOutput.Back -> state = MyState.Initial
}
}
}
// Child with Nothing output (no handler needed)
val childScreen = context.renderChild(ChildWorkflow, props = childProps)
Side Effects
For coroutine work that doesn't produce workflow output:
context.runningSideEffect("trackScreen") {
analytics.trackScreenView("my_screen")
}
Critical Rules
- •Never perform side effects in
render()—renderis called multiple times per state. UserunningWorkerorrunningSideEffect. - •Don't capture
renderStatein lambdas — IneventHandlerandactionblocks, use thestateproperty from theUpdaterreceiver, not therenderStateparameter. - •Always provide
nametoeventHandler— Required for Compose stability and debugging. - •Use
setOutput()to emit output — Call at most once per action. - •Return
nullfromsnapshotStateunless you need state persistence across process death.
Naming Conventions
| Type | Convention | Example |
|---|---|---|
| Workflow | [Feature]Workflow | LoginWorkflow |
| Props | [Feature]Props or Unit | LoginProps |
| State | [Feature]State or nested type | LoginState |
| Output | [Feature]Output or Nothing | LoginOutput |
| Rendering | [Feature]Screen (separate file) | LoginScreen |
File Organization
feature/ ├── MyWorkflow.kt # Workflow + Props + State + Output └── MyScreen.kt # Rendering class (separate file)
Required Imports
// Core workflow types import com.squareup.workflow1.StatefulWorkflow // or StatelessWorkflow import com.squareup.workflow1.Snapshot import com.squareup.workflow1.action // Workers import com.squareup.workflow1.Worker import com.squareup.workflow1.runningWorker import com.squareup.workflow1.asWorker // UI rendering import com.squareup.workflow1.ui.Screen // Factory functions import com.squareup.workflow1.Workflow import com.squareup.workflow1.stateful import com.squareup.workflow1.stateless
Documentation
- •Main docs: https://square.github.io/workflow/
- •Kotlin API: https://square.github.io/workflow/kotlin/api/htmlMultiModule/
- •Tutorial: https://github.com/square/workflow-kotlin/tree/main/samples/tutorial
Output
When creating a workflow, always:
- •Determine whether StatefulWorkflow or StatelessWorkflow is needed
- •Define Props, State (if stateful), Output, and Rendering types
- •Create the workflow class with proper type parameters
- •Create the Screen rendering in a separate file
- •Use
eventHandler("name")for UI events - •Use Workers for async operations
- •Ensure all imports are correct