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

TypeWords provides a comprehensive runtime API in 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 establishes the shape of valid configurations. Second, the runtime factory in 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, 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, 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
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 confirm the storage key is PracticeFlowV2.

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 show that resolveFlowInput retrieves the active ID when passed the string 'custom'.

// 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.

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.

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

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):

// 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
  • 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. 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. The storage format maps flow IDs to their configuration objects and metadata, enabling persistence across browser sessions without requiring a backend database.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →