defineFlow()
The defineFlow() function is the primary API for creating typed flow definitions.
Import
Section titled “Import”import { defineFlow } from "@useflow/react";Signature
Section titled “Signature”function defineFlow( definition: FlowDefinition): RuntimeFlowDefinition<FlowDefinition, {}>
// With runtime configuration:defineFlow(definition).with<TContext>(runtimeConfig): RuntimeFlowDefinition<FlowDefinition, TContext>Parameters
Section titled “Parameters”definition
Section titled “definition”The flow definition object containing metadata and step configuration.
{ id: string; // Unique identifier for the flow start: string; // Starting step name version?: string; // Optional version for migrations variantId?: string; // Optional variant identifier steps: Record<string, StepDefinition>; // Step configuration}Step Definition:
type StepDefinition = { next?: string | string[]; // Single step or array of possible steps}.with<TContext>(runtimeConfig) (optional)
Section titled “.with<TContext>(runtimeConfig) (optional)”Chain method to add runtime configuration with a specific context type. This is where you define:
- Context type - The shape of your flow’s shared state
- Resolvers - Functions that determine branching logic
- Migration - Function to handle version upgrades
Parameter: Function that receives type-safe step references and returns configuration:
(steps: StepRefs) => { migration?: MigrateFunction<TContext>; resolvers?: ResolverMap;}When to use:
- ✅ Use when you need context-driven navigation (resolvers)
- ✅ Use when you need state migrations (versioning)
- ✅ Use to explicitly type your context with
TContext - ❌ Not needed for simple linear flows without branching
Return value
Section titled “Return value”Returns a RuntimeFlowDefinition object with:
{ id: string; config: FlowDefinition; runtimeConfig?: FlowRuntimeConfig<TContext>; useFlowState: <TStep extends string>(options: { step: TStep }) => UseFlowReturn; with: <TContext>(runtimeConfig) => RuntimeFlowDefinition<FlowDefinition, TContext>;}Examples
Section titled “Examples”Basic flow
Section titled “Basic flow”import { defineFlow } from "@useflow/react";
type MyContext = { name: string; email: string;};
const flow = defineFlow({ id: "my-flow", start: "welcome", steps: { welcome: { next: "profile" }, profile: { next: "complete" }, complete: {} }}).with<MyContext>({});Flow with branching
Section titled “Flow with branching”type MyContext = { type: "business" | "personal";};
const flow = defineFlow({ id: "branching-flow", start: "userType", steps: { userType: { next: ["business", "personal"] }, business: { next: "complete" }, personal: { next: "complete" }, complete: {} }}).with<MyContext>((steps) => ({ resolvers: { userType: (ctx) => ctx.type === "business" ? steps.business : steps.personal }}));Flow with versioning
Section titled “Flow with versioning”type MyContext = { email: string;};
const flow = defineFlow({ id: "onboarding", version: "v2", start: "welcome", steps: { welcome: { next: "profile" }, profile: { next: "complete" }, complete: {} }}).with<MyContext>(() => ({ migration: (state, fromVersion) => { if (fromVersion === "v1") { return { ...state, context: { ...state.context, email: state.context.emailAddress // Rename field } }; } return null; }}));Flow with multiple variants
Section titled “Flow with multiple variants”const standardFlow = defineFlow({ id: "onboarding", variantId: "standard", start: "welcome", steps: { welcome: { next: "verification" }, verification: { next: "profile" }, profile: { next: "complete" }, complete: {} }});
const expressFlow = defineFlow({ id: "onboarding", variantId: "express", start: "welcome", steps: { welcome: { next: "profile" }, profile: { next: "complete" }, complete: {} }});Builder pattern
Section titled “Builder pattern”defineFlow() uses a builder pattern - it returns a RuntimeFlowDefinition that you can use directly or enhance with .with():
Basic flow (no runtime config)
Section titled “Basic flow (no runtime config)”// Simple flow - no branching, no migrationsconst flow = defineFlow({ id: "simple", start: "step1", steps: { step1: { next: "step2" }, step2: {} }});
// Use directly - no .with() needed<Flow flow={flow} initialContext={{}} />With runtime config
Section titled “With runtime config”type MyContext = { userType: "business" | "personal" };
// Chain .with() to add resolvers and context typeconst flow = defineFlow({ id: "branching", start: "userType", steps: { userType: { next: ["business", "personal"] }, business: { next: "complete" }, personal: { next: "complete" }, complete: {} }}).with<MyContext>((steps) => ({ resolvers: { userType: (ctx) => ctx.userType === "business" ? steps.business : steps.personal }}));Key Points:
defineFlow()returns aRuntimeFlowDefinition.with<TContext>()returns a newRuntimeFlowDefinitionwith context type- Both return values have the same API (config, useFlowState, etc.)
- Immutable -
.with()creates a new instance
Type safety
Section titled “Type safety”The defineFlow function provides full type inference:
const flow = defineFlow({ id: "example", start: "step1", steps: { step1: { next: "step2" }, step2: { next: ["step3", "step4"] }, step3: { next: "step5" }, step4: { next: "step5" }, step5: {} }});
// In components:function Step2() { const { next } = flow.useFlowState({ step: "step2" });
next("step3"); // ✅ Valid next("step4"); // ✅ Valid next("step1"); // ❌ TypeScript error: not in next array}Runtime configuration
Section titled “Runtime configuration”Resolver functions
Section titled “Resolver functions”Define branching logic for steps with multiple next options:
defineFlow({ /* definition */ }) .with<MyContext>((steps) => ({ resolvers: { stepName: (context) => { // Return one of the step references return context.condition ? steps.stepA : steps.stepB; } } }))Migration functions
Section titled “Migration functions”Handle version upgrades:
defineFlow({ version: "v2", /* ... */}).with<MyContext>(() => ({ migration: (state, fromVersion) => { if (fromVersion === "v1") { // Transform state return { ...state, context: transformContext(state.context) }; } return null; // Discard unknown versions }}))Best practices
Section titled “Best practices”1. Use descriptive IDs
Section titled “1. Use descriptive IDs”// ✅ Good: Clear identifierdefineFlow({ id: "checkout-flow" })
// ❌ Bad: Vague identifierdefineFlow({ id: "flow1" })2. Always version persisted flows
Section titled “2. Always version persisted flows”// ✅ Good: Includes versiondefineFlow({ id: "onboarding", version: "v1"})3. Keep step names consistent
Section titled “3. Keep step names consistent”// ✅ Good: Consistent namingdefineFlow({ steps: { userProfile: { next: "userPreferences" }, userPreferences: { next: "userComplete" } }})
// ❌ Inconsistent namingdefineFlow({ steps: { profile: { next: "prefs" }, prefs: { next: "done" } }})4. Define terminal steps explicitly
Section titled “4. Define terminal steps explicitly”// ✅ Good: Clear terminal stepdefineFlow({ steps: { welcome: { next: "profile" }, profile: { next: "complete" }, complete: {} // Terminal step }})See also
Section titled “See also”- Flow Component - Render flows
- useFlowState Hook - Access flow state
- Type Safety - Type Safety guide
- Migrations - Version management