Project

flowengine

0.0
No release in over 3 years
FlowEngine provides a DSL for defining multi-step flows as directed graphs with conditional branching, an AST-based rule evaluator, and a pure-Ruby runtime engine. No framework dependencies.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Development

~> 2.0
~> 0.22
>= 0
 Project Readme

FlowEngine

RSpec RuboCop

A declarative flow engine for building rules-driven wizards and intake forms in pure Ruby.

FlowEngine lets you define multi-step flows as directed graphs with conditional branching, evaluate transitions using an AST-based rule system, and collect structured answers through a stateful runtime engine — all without framework dependencies.

This is not a form builder. It's a Form Definition Engine that separates flow logic, data schema, and UI rendering into independent concerns.

Installation

Add to your Gemfile:

gem "flowengine"

Or install directly:

gem install flowengine

Quick Start

require "flowengine"

# 1. Define a flow
definition = FlowEngine.define do
  start :name

  step :name do
    type :text
    question "What is your name?"
    transition to: :age
  end

  step :age do
    type :number
    question "How old are you?"
    transition to: :beverage, if_rule: greater_than(:age, 20)
    transition to: :thanks
  end

  step :beverage do
    type :single_select
    question "Pick a drink."
    options %w[Beer Wine Cocktail]
    transition to: :thanks
  end

  step :thanks do
    type :text
    question "Thank you for your responses!"
  end
end

# 2. Run the engine
engine = FlowEngine::Engine.new(definition)

engine.answer("Alice")       # :name    -> :age
engine.answer(25)            # :age     -> :beverage  (25 > 20)
engine.answer("Wine")        # :beverage -> :thanks
engine.answer("ok")          # :thanks  -> finished

engine.finished?  # => true
engine.answers
# => { name: "Alice", age: 25, beverage: "Wine", thanks: "ok" }
engine.history
# => [:name, :age, :beverage, :thanks]

If Alice were 18 instead, the engine would skip :beverage entirely:

engine.answer("Alice")       # :name    -> :age
engine.answer(18)            # :age     -> :thanks  (18 is NOT > 20)
engine.answer("ok")          # :thanks  -> finished

engine.answers
# => { name: "Alice", age: 18, thanks: "ok" }
engine.history
# => [:name, :age, :thanks]

Architecture

                 +-------------------+
                 |    FlowEngine     |
                 |-------------------|
                 | DSL               |
                 | Definition        |
                 | Node              |
                 | Transition        |
                 | Rules (AST)       |
                 | Evaluator         |
                 | Engine            |
                 | Validation layer  |
                 | Graph Exporter    |
                 +-------------------+
                          ^
            +-------------+-------------+
            |                           |
+------------------------+   +---------------------------+
| flowengine-cli         |   | flowengine-rails          |
|------------------------|   |---------------------------|
| TTY UI adapter         |   | Rails Engine              |
| JSON output            |   | ActiveRecord persistence  |
| Mermaid export         |   | Web views/components      |
+------------------------+   +---------------------------+

The core has zero UI logic, zero DB logic, and zero framework dependencies. Adapters translate input/output, persist state, and render UI.

Core Components

Component Responsibility
FlowEngine.define DSL entry point; returns a frozen Definition
Definition Immutable container of the flow graph (nodes + start step)
Node A single step: type, question, options/fields, transitions, visibility
Transition A directed edge with an optional rule condition
Rules::* AST nodes for conditional logic (Contains, Equals, All, etc.)
Evaluator Evaluates rules against the current answer store
Engine Stateful runtime: tracks current step, answers, and history
Validation::Adapter Interface for pluggable validation (dry-validation, JSON Schema, etc.)
Graph::MermaidExporter Exports the flow definition as a Mermaid diagram

The DSL

Defining a Flow

Every flow starts with FlowEngine.define, which returns a frozen, immutable Definition:

definition = FlowEngine.define do
  start :first_step     # Required: which node to begin at

  step :first_step do
    # step configuration...
  end

  step :second_step do
    # step configuration...
  end
end

Step Configuration

Inside a step block, you have access to:

Method Purpose Example
type The input type (for UI adapters) :text, :number, :single_select, :multi_select, :number_matrix
question The prompt shown to the user "What is your filing status?"
options Available choices (for select types) %w[W2 1099 Business]
fields Named fields (for matrix types) %w[RealEstate SCorp LLC]
transition Where to go next (with optional condition) transition to: :next_step, if_rule: equals(:field, "value")
visible_if Visibility rule (for DAG mode) visible_if contains(:income, "Rental")

Step Types

Step types are semantic labels consumed by UI adapters. The engine itself is type-agnostic — it stores whatever value you pass to engine.answer(value). The types communicate intent to renderers:

step :filing_status do
  type :single_select                    # One choice from a list
  question "What is your filing status?"
  options %w[single married_filing_jointly married_filing_separately head_of_household]
  transition to: :dependents
end

step :income_types do
  type :multi_select                     # Multiple choices from a list
  question "Select all income types."
  options %w[W2 1099 Business Investment Rental]
  transition to: :business, if_rule: contains(:income_types, "Business")
  transition to: :summary
end

step :dependents do
  type :number                           # A numeric value
  question "How many dependents?"
  transition to: :income_types
end

step :business_details do
  type :number_matrix                    # Multiple named numeric fields
  question "How many of each business type?"
  fields %w[RealEstate SCorp CCorp Trust LLC]
  transition to: :summary
end

step :notes do
  type :text                             # Free-form text
  question "Any additional notes?"
  transition to: :summary
end

Transitions

Transitions define the edges of the flow graph. They are evaluated in order — the first matching transition wins:

step :income_types do
  type :multi_select
  question "Select income types."
  options %w[W2 1099 Business Investment Rental]

  # Conditional transitions (checked in order)
  transition to: :business_count,      if_rule: contains(:income_types, "Business")
  transition to: :investment_details,  if_rule: contains(:income_types, "Investment")
  transition to: :rental_details,      if_rule: contains(:income_types, "Rental")

  # Unconditional fallback (always matches)
  transition to: :state_filing
end

Key behavior: Only the first matching transition fires. If the user selects ["Business", "Investment", "Rental"], the engine goes to :business_count first. The subsequent steps must themselves include transitions to eventually reach :investment_details and :rental_details.

A transition with no if_rule: always matches — use it as a fallback at the end of the list.

Visibility Rules

Nodes can have visibility conditions for DAG-mode rendering, where a UI adapter shows/hides steps dynamically:

step :spouse_income do
  type :number
  question "What is your spouse's annual income?"
  visible_if equals(:filing_status, "married_filing_jointly")
  transition to: :deductions
end

The engine exposes this via node.visible?(answers), which returns true when the rule is satisfied (or when no visibility rule is set).

Rule System

Rules are AST objects — not hashes, not strings. They are immutable, composable, and evaluate polymorphically.

Atomic Rules

Rule DSL Helper Evaluates String Representation
Contains contains(:field, "val") Array(answers[:field]).include?("val") val in field
Equals equals(:field, "val") answers[:field] == "val" field == val
GreaterThan greater_than(:field, 10) answers[:field].to_i > 10 field > 10
LessThan less_than(:field, 5) answers[:field].to_i < 5 field < 5
NotEmpty not_empty(:field) answers[:field] is not nil and not empty field is not empty

Composite Rules

Combine atomic rules with boolean logic:

# AND — all conditions must be true
transition to: :special_review,
           if_rule: all(
             equals(:filing_status, "married_filing_jointly"),
             contains(:income_types, "Business"),
             greater_than(:business_count, 2)
           )

# OR — at least one condition must be true
transition to: :alt_path,
           if_rule: any(
             contains(:income_types, "Investment"),
             contains(:income_types, "Rental")
           )

Composites nest arbitrarily:

transition to: :complex_review,
           if_rule: all(
             equals(:filing_status, "married_filing_jointly"),
             any(
               greater_than(:business_count, 3),
               contains(:income_types, "Rental")
             ),
             not_empty(:dependents)
           )

How Rules Evaluate

Every rule implements evaluate(answers) where answers is the engine's hash of { step_id => value }:

rule = FlowEngine::Rules::Contains.new(:income_types, "Business")
rule.evaluate({ income_types: ["W2", "Business"] })  # => true
rule.evaluate({ income_types: ["W2"] })               # => false

rule = FlowEngine::Rules::All.new(
  FlowEngine::Rules::Equals.new(:status, "married"),
  FlowEngine::Rules::GreaterThan.new(:dependents, 0)
)
rule.evaluate({ status: "married", dependents: 2 })   # => true
rule.evaluate({ status: "single", dependents: 2 })    # => false

Engine API

Creating and Running

definition = FlowEngine.define { ... }
engine = FlowEngine::Engine.new(definition)

Methods

Method Returns Description
engine.current_step_id Symbol or nil The ID of the current step
engine.current_step Node or nil The current Node object
engine.answer(value) nil Records the answer and advances
engine.finished? Boolean true when there are no more steps
engine.answers Hash All collected answers { step_id => value }
engine.history Array<Symbol> Ordered list of visited step IDs
engine.definition Definition The immutable flow definition

Error Handling

# Answering after the flow is complete
engine.answer("extra")
# => raises FlowEngine::AlreadyFinishedError

# Referencing an unknown step in a definition
definition.step(:nonexistent)
# => raises FlowEngine::UnknownStepError

# Invalid definition (start step doesn't exist)
FlowEngine.define do
  start :missing
  step :other do
    type :text
    question "Hello"
  end
end
# => raises FlowEngine::DefinitionError

Validation

The engine accepts a pluggable validator via the adapter pattern. The core gem ships with a NullAdapter (always passes) and defines the interface for custom adapters:

# The adapter interface
class FlowEngine::Validation::Adapter
  def validate(node, input)
    # Must return a FlowEngine::Validation::Result
    raise NotImplementedError
  end
end

# Result object
FlowEngine::Validation::Result.new(valid: true, errors: [])
FlowEngine::Validation::Result.new(valid: false, errors: ["must be a number"])

Custom Validator Example

class MyValidator < FlowEngine::Validation::Adapter
  def validate(node, input)
    errors = []

    case node.type
    when :number
      errors << "must be a number" unless input.is_a?(Numeric)
    when :single_select
      errors << "invalid option" unless node.options&.include?(input)
    when :multi_select
      unless input.is_a?(Array) && input.all? { |v| node.options&.include?(v) }
        errors << "invalid options"
      end
    end

    FlowEngine::Validation::Result.new(valid: errors.empty?, errors: errors)
  end
end

engine = FlowEngine::Engine.new(definition, validator: MyValidator.new)
engine.answer("not_a_number")  # => raises FlowEngine::ValidationError

Mermaid Diagram Export

Export any flow definition as a Mermaid flowchart:

exporter = FlowEngine::Graph::MermaidExporter.new(definition)
puts exporter.export

Output:

flowchart TD
  name["What is your name?"]
  name --> age
  age["How old are you?"]
  age -->|"age > 20"| beverage
  age --> thanks
  beverage["Pick a drink."]
  beverage --> thanks
  thanks["Thank you for your responses!"]
Loading

Complete Example: Tax Intake Flow

Here's a realistic 17-step tax intake wizard that demonstrates every feature of the DSL.

Flow Definition

tax_intake = FlowEngine.define do
  start :filing_status

  step :filing_status do
    type :single_select
    question "What is your filing status for 2025?"
    options %w[single married_filing_jointly married_filing_separately head_of_household]
    transition to: :dependents
  end

  step :dependents do
    type :number
    question "How many dependents do you have?"
    transition to: :income_types
  end

  step :income_types do
    type :multi_select
    question "Select all income types that apply to you in 2025."
    options %w[W2 1099 Business Investment Rental Retirement]
    transition to: :business_count,      if_rule: contains(:income_types, "Business")
    transition to: :investment_details,  if_rule: contains(:income_types, "Investment")
    transition to: :rental_details,      if_rule: contains(:income_types, "Rental")
    transition to: :state_filing
  end

  step :business_count do
    type :number
    question "How many total businesses do you own or are a partner in?"
    transition to: :complex_business_info, if_rule: greater_than(:business_count, 2)
    transition to: :business_details
  end

  step :complex_business_info do
    type :text
    question "With more than 2 businesses, please provide your primary EIN and a brief description of each entity."
    transition to: :business_details
  end

  step :business_details do
    type :number_matrix
    question "How many of each business type do you own?"
    fields %w[RealEstate SCorp CCorp Trust LLC]
    transition to: :investment_details, if_rule: contains(:income_types, "Investment")
    transition to: :rental_details,    if_rule: contains(:income_types, "Rental")
    transition to: :state_filing
  end

  step :investment_details do
    type :multi_select
    question "What types of investments do you hold?"
    options %w[Stocks Bonds Crypto RealEstate MutualFunds]
    transition to: :crypto_details,  if_rule: contains(:investment_details, "Crypto")
    transition to: :rental_details,  if_rule: contains(:income_types, "Rental")
    transition to: :state_filing
  end

  step :crypto_details do
    type :text
    question "Please describe your cryptocurrency transactions (exchanges used, approximate number of transactions)."
    transition to: :rental_details, if_rule: contains(:income_types, "Rental")
    transition to: :state_filing
  end

  step :rental_details do
    type :number_matrix
    question "Provide details about your rental properties."
    fields %w[Residential Commercial Vacation]
    transition to: :state_filing
  end

  step :state_filing do
    type :multi_select
    question "Which states do you need to file in?"
    options %w[California NewYork Texas Florida Illinois Other]
    transition to: :foreign_accounts
  end

  step :foreign_accounts do
    type :single_select
    question "Do you have any foreign financial accounts (bank accounts, securities, or financial assets)?"
    options %w[yes no]
    transition to: :foreign_account_details, if_rule: equals(:foreign_accounts, "yes")
    transition to: :deduction_types
  end

  step :foreign_account_details do
    type :number
    question "How many foreign accounts do you have?"
    transition to: :deduction_types
  end

  step :deduction_types do
    type :multi_select
    question "Which additional deductions apply to you?"
    options %w[Medical Charitable Education Mortgage None]
    transition to: :charitable_amount, if_rule: contains(:deduction_types, "Charitable")
    transition to: :contact_info
  end

  step :charitable_amount do
    type :number
    question "What is your total estimated charitable contribution amount for 2025?"
    transition to: :charitable_documentation, if_rule: greater_than(:charitable_amount, 5000)
    transition to: :contact_info
  end

  step :charitable_documentation do
    type :text
    question "For charitable contributions over $5,000, please list the organizations and amounts."
    transition to: :contact_info
  end

  step :contact_info do
    type :text
    question "Please provide your contact information (name, email, phone)."
    transition to: :review
  end

  step :review do
    type :text
    question "Thank you! Please review your information. Type 'confirm' to submit."
  end
end

Scenario 1: Maximum Path (17 steps visited)

A married filer with all income types, 4 businesses, crypto, rentals, foreign accounts, and high charitable giving:

engine = FlowEngine::Engine.new(tax_intake)

engine.answer("married_filing_jointly")
engine.answer(3)
engine.answer(%w[W2 1099 Business Investment Rental Retirement])
engine.answer(4)
engine.answer("EIN: 12-3456789. Entities: Alpha LLC, Beta SCorp, Gamma LLC, Delta CCorp")
engine.answer({ "RealEstate" => 1, "SCorp" => 1, "CCorp" => 1, "Trust" => 0, "LLC" => 2 })
engine.answer(%w[Stocks Bonds Crypto RealEstate])
engine.answer("Coinbase and Kraken, approximately 150 transactions in 2025")
engine.answer({ "Residential" => 2, "Commercial" => 1, "Vacation" => 0 })
engine.answer(%w[California NewYork])
engine.answer("yes")
engine.answer(3)
engine.answer(%w[Medical Charitable Education Mortgage])
engine.answer(12_000)
engine.answer("Red Cross: $5,000; Habitat for Humanity: $4,000; Local Food Bank: $3,000")
engine.answer("Jane Smith, jane.smith@example.com, 555-123-4567")
engine.answer("confirm")

Collected data:

{
  "filing_status": "married_filing_jointly",
  "dependents": 3,
  "income_types": ["W2", "1099", "Business", "Investment", "Rental", "Retirement"],
  "business_count": 4,
  "complex_business_info": "EIN: 12-3456789. Entities: Alpha LLC, Beta SCorp, Gamma LLC, Delta CCorp",
  "business_details": {
    "RealEstate": 1,
    "SCorp": 1,
    "CCorp": 1,
    "Trust": 0,
    "LLC": 2
  },
  "investment_details": ["Stocks", "Bonds", "Crypto", "RealEstate"],
  "crypto_details": "Coinbase and Kraken, approximately 150 transactions in 2025",
  "rental_details": {
    "Residential": 2,
    "Commercial": 1,
    "Vacation": 0
  },
  "state_filing": ["California", "NewYork"],
  "foreign_accounts": "yes",
  "foreign_account_details": 3,
  "deduction_types": ["Medical", "Charitable", "Education", "Mortgage"],
  "charitable_amount": 12000,
  "charitable_documentation": "Red Cross: $5,000; Habitat for Humanity: $4,000; Local Food Bank: $3,000",
  "contact_info": "Jane Smith, jane.smith@example.com, 555-123-4567",
  "review": "confirm"
}

Path taken (all 17 steps):

filing_status -> dependents -> income_types -> business_count ->
complex_business_info -> business_details -> investment_details ->
crypto_details -> rental_details -> state_filing -> foreign_accounts ->
foreign_account_details -> deduction_types -> charitable_amount ->
charitable_documentation -> contact_info -> review

Scenario 2: Minimum Path (8 steps visited)

A single filer, W2 income only, no special deductions:

engine = FlowEngine::Engine.new(tax_intake)

engine.answer("single")
engine.answer(0)
engine.answer(["W2"])
engine.answer(["Texas"])
engine.answer("no")
engine.answer(["None"])
engine.answer("John Doe, john.doe@example.com, 555-987-6543")
engine.answer("confirm")

Collected data:

{
  "filing_status": "single",
  "dependents": 0,
  "income_types": ["W2"],
  "state_filing": ["Texas"],
  "foreign_accounts": "no",
  "deduction_types": ["None"],
  "contact_info": "John Doe, john.doe@example.com, 555-987-6543",
  "review": "confirm"
}

Path taken (8 steps — skipped 9 steps):

filing_status -> dependents -> income_types -> state_filing ->
foreign_accounts -> deduction_types -> contact_info -> review

Skipped: business_count, complex_business_info, business_details, investment_details, crypto_details, rental_details, foreign_account_details, charitable_amount, charitable_documentation.

Scenario 3: Medium Path (12 steps visited)

Married, with business + investment income, low charitable giving:

engine = FlowEngine::Engine.new(tax_intake)

engine.answer("married_filing_separately")
engine.answer(1)
engine.answer(%w[W2 Business Investment])
engine.answer(2)                                    # <= 2 businesses, no complex_business_info
engine.answer({ "RealEstate" => 0, "SCorp" => 1, "CCorp" => 0, "Trust" => 0, "LLC" => 1 })
engine.answer(%w[Stocks Bonds MutualFunds])          # no Crypto, no crypto_details
engine.answer(%w[California Illinois])
engine.answer("no")                                  # no foreign accounts
engine.answer(%w[Charitable Mortgage])
engine.answer(3000)                                  # <= 5000, no documentation needed
engine.answer("Alice Johnson, alice.j@example.com, 555-555-0100")
engine.answer("confirm")

Collected data:

{
  "filing_status": "married_filing_separately",
  "dependents": 1,
  "income_types": ["W2", "Business", "Investment"],
  "business_count": 2,
  "business_details": {
    "RealEstate": 0,
    "SCorp": 1,
    "CCorp": 0,
    "Trust": 0,
    "LLC": 1
  },
  "investment_details": ["Stocks", "Bonds", "MutualFunds"],
  "state_filing": ["California", "Illinois"],
  "foreign_accounts": "no",
  "deduction_types": ["Charitable", "Mortgage"],
  "charitable_amount": 3000,
  "contact_info": "Alice Johnson, alice.j@example.com, 555-555-0100",
  "review": "confirm"
}

Path taken (12 steps):

filing_status -> dependents -> income_types -> business_count ->
business_details -> investment_details -> state_filing ->
foreign_accounts -> deduction_types -> charitable_amount ->
contact_info -> review

Scenario 4: Rental + Foreign Accounts (10 steps visited)

Head of household, 1099 + rental income, foreign accounts, no charitable:

engine = FlowEngine::Engine.new(tax_intake)

engine.answer("head_of_household")
engine.answer(2)
engine.answer(%w[1099 Rental])
engine.answer({ "Residential" => 1, "Commercial" => 0, "Vacation" => 1 })
engine.answer(["Florida"])
engine.answer("yes")
engine.answer(1)
engine.answer(%w[Medical Education])
engine.answer("Bob Lee, bob@example.com, 555-000-1111")
engine.answer("confirm")

Collected data:

{
  "filing_status": "head_of_household",
  "dependents": 2,
  "income_types": ["1099", "Rental"],
  "rental_details": {
    "Residential": 1,
    "Commercial": 0,
    "Vacation": 1
  },
  "state_filing": ["Florida"],
  "foreign_accounts": "yes",
  "foreign_account_details": 1,
  "deduction_types": ["Medical", "Education"],
  "contact_info": "Bob Lee, bob@example.com, 555-000-1111",
  "review": "confirm"
}

Path taken (10 steps):

filing_status -> dependents -> income_types -> rental_details ->
state_filing -> foreign_accounts -> foreign_account_details ->
deduction_types -> contact_info -> review

Comparing Collected Data Across Paths

The shape of the collected data depends entirely on which path the user takes through the graph. Here's a side-by-side of which keys appear in each scenario:

Answer Key Max (17) Min (8) Medium (12) Rental (10)
filing_status x x x x
dependents x x x x
income_types x x x x
business_count x x
complex_business_info x
business_details x x
investment_details x x
crypto_details x
rental_details x x
state_filing x x x x
foreign_accounts x x x x
foreign_account_details x x
deduction_types x x x x
charitable_amount x x
charitable_documentation x
contact_info x x x x
review x x x x

Composing Complex Rules

Example: Multi-Condition Branching

definition = FlowEngine.define do
  start :status

  step :status do
    type :single_select
    question "Filing status?"
    options %w[single married]
    transition to: :dependents
  end

  step :dependents do
    type :number
    question "How many dependents?"
    transition to: :income
  end

  step :income do
    type :multi_select
    question "Income types?"
    options %w[W2 Business]

    # All three conditions must be true
    transition to: :special_review,
               if_rule: all(
                 equals(:status, "married"),
                 contains(:income, "Business"),
                 not_empty(:income)
               )

    # At least one condition must be true
    transition to: :alt_review,
               if_rule: any(
                 less_than(:dependents, 2),
                 contains(:income, "W2")
               )

    # Unconditional fallback
    transition to: :default_review
  end

  step :special_review do
    type :text
    question "Married with business income - special review required."
  end

  step :alt_review do
    type :text
    question "Alternative review path."
  end

  step :default_review do
    type :text
    question "Default review."
  end
end

The four possible outcomes:

Inputs Path Why
married, 0 deps, [W2, Business] :special_review all() satisfied: married + Business + not empty
single, 0 deps, [W2] :alt_review all() fails (not married); any() passes (has W2)
single, 1 dep, [Business] :alt_review all() fails; any() passes (deps < 2)
single, 3 deps, [Business] :default_review all() fails; any() fails (deps not < 2, no W2)

Mermaid Diagram of the Tax Intake Flow

Example

Expand to see Mermaid source
flowchart BT
    filing_status["What is your filing status for 2025?"] --> dependents["How many dependents do you have?"]
    dependents --> income_types["Select all income types that apply to you in 2025."]
    income_types -- Business in income_types --> business_count["How many total businesses do you own or are a part..."]
    income_types -- Investment in income_types --> investment_details["What types of investments do you hold?"]
    income_types -- Rental in income_types --> rental_details["Provide details about your rental properties."]
    income_types --> state_filing["Which states do you need to file in?"]
    business_count -- business_count > 2 --> complex_business_info["With more than 2 businesses, please provide your p..."]
    business_count --> business_details["How many of each business type do you own?"]
    complex_business_info --> business_details
    business_details -- Investment in income_types --> investment_details
    business_details -- Rental in income_types --> rental_details
    business_details --> state_filing
    investment_details -- Crypto in investment_details --> crypto_details["Please describe your cryptocurrency transactions (..."]
    investment_details -- Rental in income_types --> rental_details
    investment_details --> state_filing
    crypto_details -- Rental in income_types --> rental_details
    crypto_details --> state_filing
    rental_details --> state_filing
    state_filing --> foreign_accounts["Do you have any foreign financial accounts (bank a..."]
    foreign_accounts -- "foreign_accounts == yes" --> foreign_account_details["How many foreign accounts do you have?"]
    foreign_accounts --> deduction_types["Which additional deductions apply to you?"]
    foreign_account_details --> deduction_types
    deduction_types -- Charitable in deduction_types --> charitable_amount["What is your total estimated charitable contributi..."]
    deduction_types --> contact_info["Please provide your contact information (name, ema..."]
    charitable_amount -- charitable_amount > 5000 --> charitable_documentation["For charitable contributions over $5,000, please l..."]
    charitable_amount --> contact_info
    charitable_documentation --> contact_info
    contact_info --> review@{ label: "Thank you! Please review your information. Type 'c..." }

    review@{ shape: rect}
Loading

Ecosystem

FlowEngine is the core of a three-gem architecture:

Gem Purpose
flowengine (this gem) Core engine — pure Ruby, no Rails, no DB, no UI
flowengine-cli Terminal wizard adapter using TTY Toolkit + Dry::CLI
flowengine-rails Rails Engine with ActiveRecord persistence and web views

Development

bundle install
bundle exec rspec

License

The gem is available as open source under the terms of the MIT License.