# How to Implement Custom Practice Flows in TypeWords Using the Runtime API

> Learn to implement custom practice flows in TypeWords with the runtime API. Define, persist, and execute your own flows using PracticeFlowConfig objects and createPracticeFlowRuntime.

- Repository: [Zyronon/TypeWords](https://github.com/zyronon/TypeWords)
- Tags: how-to-guide
- Published: 2026-09-03

---

**TypeWords provides a comprehensive runtime API in [`practice-flow-runtime.ts`](https://github.com/zyronon/TypeWords/blob/main/practice-flow-runtime.ts) that enables developers to create, persist, and execute custom practice flows by defining valid `PracticeFlowConfig` objects and managing them through `createPracticeFlowRuntime()`.**

The zyronon/TypeWords repository exposes a flexible runtime API that transforms static configuration into dynamic practice sessions. By leveraging the composables found in `app/core/composables/practice-words/`, developers can build personalized learning experiences without modifying core UI components. This guide walks through the complete implementation process using the actual source code structure.

## Understanding the TypeWords Practice Flow Architecture

The runtime API separates concerns into three distinct layers. First, the **flow configuration schema** defined in [`practice-flow-types.ts`](https://github.com/zyronon/TypeWords/blob/main/practice-flow-types.ts) establishes the shape of valid configurations. Second, the **runtime factory** in [`practice-flow-runtime.ts`](https://github.com/zyronon/TypeWords/blob/main/practice-flow-runtime.ts) creates isolated session instances and handles state progression. Third, the **persistent store** manages custom flow definitions in `localStorage` under the key `PracticeFlowV2`.

The system validates configurations through `isValidFlowConfig` before persistence. Built-in flows (system, free, review) live in [`practice-flow-config.ts`](https://github.com/zyronon/TypeWords/blob/main/practice-flow-config.ts), but the runtime API specifically exposes methods for custom flow management including `saveUserFlow`, `setActiveCustomFlowId`, and `deleteUserFlow`.

## Defining a Valid Custom Flow Configuration

A custom flow must conform to the `PracticeFlowConfig` interface and pass validation checks before the runtime accepts it.

### Schema Requirements and Validation

According to lines 20-31 of [`practice-flow-runtime.ts`](https://github.com/zyronon/TypeWords/blob/main/practice-flow-runtime.ts), every custom flow requires:

- `id`: A unique string identifier (e.g., `'myCustom'`)
- `label`: Human-readable display name
- `version`: Must equal `CURRENT_FLOW_VERSION` (currently `6`)
- `mode`: Must be `WordPracticeMode.Custom` (enum value `5`)
- `nodes`: An array containing at least one node object

Each node must specify a `source` from the allowed set: `taskNew`, `taskReview`, `current`, or `wrongWords`. Nodes also require a non-empty `steps` array defining the practice templates to execute.

### Node and Step Structure

Steps reference template IDs defined in `STEP_TEMPLATE_META`: `followWrite`, `spell`, `listen`, `dictation`, or `identify`. Configure optional behavior through:

- `wordAdvance`: Either `{type:'increment'}` for sequential progression or a `wordLoop` definition (lines 85-99)
- `shuffleOnEnter`: Boolean to randomize word order when entering the step
- `onEnd`: Array of `PracticeEndAction` objects such as `wrongWordClear`

```typescript
import { WordPracticeMode } from '@/core/types/enum'

const myCustomFlow = {
  id: 'myCustom',
  version: 6,
  mode: WordPracticeMode.Custom,
  label: 'My Custom Flow',
  nodes: [
    {
      id: 'new',
      label: 'New Words',
      source: 'taskNew',
      steps: [
        {
          templateId: 'followWrite',
          wordAdvance: { type: 'increment' },
          onEnd: [],
          shuffleOnEnter: false,
        },
        {
          templateId: 'listen',
          shuffleOnEnter: true,
        },
      ],
    },
    {
      id: 'review',
      label: 'Review',
      source: 'taskReview',
      steps: [
        { templateId: 'identify', onEnd: [] },
        { templateId: 'dictation', shuffleOnEnter: true },
      ],
    },
  ],
}

```

## Persisting and Activating Custom Flows

Once defined, configurations must persist to `localStorage` and activate before use.

### Saving Flows to Local Storage

The `createPracticeFlowRuntime()` factory returns a `saveUserFlow` method that validates configurations and writes them to storage. Lines 71-73 of [`practice-flow-runtime.ts`](https://github.com/zyronon/TypeWords/blob/main/practice-flow-runtime.ts) confirm the storage key is `PracticeFlowV2`.

```typescript
import { createPracticeFlowRuntime } from '@/core/composables/practice-words/practice-flow-runtime'

const runtime = createPracticeFlowRuntime()

// Save returns void but throws 'INVALID_FLOW_CONFIG' on validation failure
runtime.saveUserFlow(myCustomFlow.id, myCustomFlow, 'My Custom Flow')

```

The second argument is the configuration object; the third argument specifies the display name shown in the UI selection interface.

### Setting the Active Custom Flow

TypeWords maintains exactly one active custom flow at a time. Lines 60-64 of [`practice-flow-runtime.ts`](https://github.com/zyronon/TypeWords/blob/main/practice-flow-runtime.ts) show that `resolveFlowInput` retrieves the active ID when passed the string `'custom'`.

```typescript
// Activate the flow for upcoming sessions
runtime.setActiveCustomFlowId('myCustom')

```

If the ID does not exist in storage, the runtime will fail when attempting to start a session with `WordPracticeMode.Custom`.

## Executing a Practice Session with the Runtime API

After activation, drive the practice UI using the runtime's session management methods.

### Starting the Session with resolveFlowStart

The `resolveFlowStart` method (lines 31-57) initializes the session state. It accepts the practice mode, a `TaskWords` payload containing `new` and `review` word arrays, and the flow identifier string.

```typescript
const taskWords = { 
  new: newWordArray, 
  review: reviewWordArray 
}

const start = runtime.resolveFlowStart(
  WordPracticeMode.Custom,
  taskWords,
  'custom'  // Signals runtime to use the active custom flow
)

console.log('Initial word set:', start.words)
console.log('Starting cursor:', start.cursor)
console.log('Total new words:', start.newWordNumber)
console.log('Total review words:', start.reviewWordNumber)

```

This method selects the first node containing words, applies shuffle settings if configured, and returns the initial cursor position along with task statistics.

### Navigating Steps and Phases

Progress through the flow using cursor advancement methods exposed by the runtime factory:

```typescript
let { cursor, complete } = { 
  cursor: start.cursor, 
  complete: false 
}

while (!complete) {
  // Resolve the current phase definition including sub-steps
  const phase = runtime.resolvePhaseByCursor(cursor)
  
  // Get primary phase details for UI rendering
  const mainPhase = runtime.getMainPhase(cursor)
  
  // Render UI according to phase.practiceType
  // Handle user input, word advancement, etc.
  
  // Advance to next step or node
  const result = runtime.advanceStepCursor(cursor)
  cursor = result.cursor
  complete = result.complete
}

```

- **`getMainPhase(cursor)`**: Returns the primary phase definition for the current step
- **`resolvePhaseByCursor(cursor)`**: Handles special sub-steps like wrong-word-clear loops
- **`advanceStepCursor(cursor)`**: Moves to the next step/node and returns completion status

## Managing and Updating Custom Flows

Update existing flows by calling `saveUserFlow` again with the same ID. The method overwrites the previous definition in `localStorage`.

Remove flows permanently using `deleteUserFlow`, which also clears the active flow if it matches the deleted ID (lines 22-26):

```typescript
// Update existing flow
runtime.saveUserFlow('myCustom', updatedFlowConfig, 'My Updated Flow')

// Delete and deactivate if currently active
runtime.deleteUserFlow('myCustom')

```

## Summary

- **Custom flows** require a `PracticeFlowConfig` object with `version: 6`, `mode: WordPracticeMode.Custom`, and valid node/step definitions as specified in [`practice-flow-types.ts`](https://github.com/zyronon/TypeWords/blob/main/practice-flow-types.ts)
- **Validation** occurs through `isValidFlowConfig` before persistence to the `PracticeFlowV2` localStorage key
- **Runtime instantiation** happens via `createPracticeFlowRuntime()`, which exposes `saveUserFlow`, `setActiveCustomFlowId`, and session management helpers
- **Session initiation** uses `resolveFlowStart` with the `'custom'` identifier to load the active flow configuration
- **Step progression** relies on `advanceStepCursor`, `getMainPhase`, and `resolvePhaseByCursor` to drive the practice UI

## Frequently Asked Questions

### What is the required version number for custom flows?

The `version` field must equal `CURRENT_FLOW_VERSION`, which is currently `6` as defined in [`practice-flow-runtime.ts`](https://github.com/zyronon/TypeWords/blob/main/practice-flow-runtime.ts). Using any other version number causes `isValidFlowConfig` to reject the configuration with an invalid version error.

### How does the runtime validate custom flow configurations?

The `saveUserFlow` method invokes `isValidFlowConfig` before writing to `localStorage`. It checks for required fields (`id`, `label`, `version`, `mode`, `nodes`), validates that `mode` matches `WordPracticeMode.Custom` (value `5`), ensures node sources are from the allowed enumeration, and verifies step template IDs exist in `STEP_TEMPLATE_META`. Validation failures throw the string `'INVALID_FLOW_CONFIG'`.

### Can I use multiple custom flows simultaneously?

No. The runtime maintains only one active custom flow at a time through `setActiveCustomFlowId`. However, you can store multiple flows in `localStorage` using `saveUserFlow` and switch between them by calling `setActiveCustomFlowId` with different IDs before starting a new session. The `listUserFlows` method returns all stored custom configurations.

### Where are custom flows stored in TypeWords?

Custom flows persist to `localStorage` under the key `PracticeFlowV2` as implemented in lines 71-73 of [`practice-flow-runtime.ts`](https://github.com/zyronon/TypeWords/blob/main/practice-flow-runtime.ts). The storage format maps flow IDs to their configuration objects and metadata, enabling persistence across browser sessions without requiring a backend database.