Qualified At Implementation Guide

Qualified.at is built around one idea: your qualification flow is data, not code. You describe the conversation you want to have with a lead — the questions, the branching, the messages in between — in the Inquirex Ruby DSL, and the platform turns that description into an embeddable copilot widget, a hosted wizard, emails, and webhooks.

The conceptual model. A Site represents one place the copilot lives — usually one website. Each site has a Qualifier: a versioned, immutable snapshot of your flow definition. Editing the DSL creates a new version, so a lead mid-conversation is never surprised by a changed flow. The flow itself is a directed graph of steps: verbs like ask and confirm collect typed answers, while say, header, btw and warning only display content. Transitions connect steps, optionally guarded by serializable rules (equals, contains, greater_than, not_empty, composable with all / any) evaluated against the answers collected so far — that is how a simple respondent answers eight questions while a complex one answers seventeen, from the same definition.

AI, where it earns its keep. A flow may open with a free-text step and an extract verb: the visitor describes their situation in plain language, a server-side model extracts structured answers matching your schema, and downstream questions with skip_if not_empty(...) rules are skipped or pre-filled. Single-choice extractions skip their question; multi-choice extractions pre-select but still show it, so the visitor can add what the model missed. If no model is configured, the flow degrades gracefully into a plain form.

After completion. When a lead finishes, post-completion action declarations run server-side — send_email builds branded emails with inert {{field}} templating — and site-level settings deliver owner notifications and webhooks (with a configurable JSON payload template) to your CRM. The whole definition round-trips losslessly between the Ruby DSL and JSON, which is what the embedded widget consumes.

The two references below are the same material available from the DSL Reference and DSL Explained buttons in the flow editor — reproduced here full-size.

Inquirex DSL Reference

Define your qualification flow using the Inquirex DSL. Each flow is built from verbs (ask, say, header, btw, warning, confirm) connected by transitions, plus completion emails that go out once the lead reaches the end.

Verbs
ask Collects: yes

Question that collects a typed answer

confirm Collects: boolean

Yes / No confirmation gate

say Collects: no

Informational message

header Collects: no

Section heading or title card

btw Collects: no

Sidebar or contextual note

warning Collects: no

Alert the user to something important

Answer Types (for ask)
:string Returns: String

Short single-line text

:text Returns: String

Long multiline text

:integer Returns: Integer

Whole number

:decimal Returns: Float

Floating-point number

:currency Returns: Float

Monetary amount

:enum Returns: String

Radio buttons (pick one)

:multi_enum Returns: Array

Checkboxes or multi-select list

:boolean Returns: true/false

Yes / No

:date Returns: String

Calendar date

:email Returns: String

Validated email address

:phone Returns: String

Phone number

Conditional Rules
  • equals(:step_id, "value") — exact match
  • contains(:step_id, "value") — array includes value
  • greater_than(:step_id, n) / less_than(:step_id, n)
  • not_empty(:step_id)
  • all(rule1, rule2) / any(rule1, rule2) — combine rules
Completion emails

send_email is a completion effect, not a conversational step. It is declared at flow level, alongside start rather than inside a verb, and is built and sent once the lead finishes the flow — so it has no transition and never appears to the lead. Pass an optional if: rule to send it only on the paths you choose, and declare as many as you need.

send_email Runs: on completion

Emails the collected answers. Requires a plan that includes send_email; on a plan without it the flow still runs normally and the message is simply skipped.

  • to from cc bcc reply_to subject — plain strings
  • text / markdown_text / html — the body, written inline as a string
  • headers — any extra mail headers

Two limits worth knowing. Bodies must be written inline — the gem also accepts text({ file: "…" }), which would read a file off the server, so it is refused here. And there is no webhook declaration: configure webhooks on the site itself, where the destination URL gets safety-checked.

send_email if: contains(:income_types, "Business") do
  to       "intake@example.com"
  reply_to "no-reply@example.com"
  subject  "New business-filer lead"
  text     "A lead completed the business path."
end
Example
Inquirex.define do
  start :filing_status

  ask :filing_status do
    type :enum
    question "What is your filing status?"
    options %w[single married head_of_household]
    transition to: :income_types
  end

  ask :income_types do
    type :multi_enum
    question "Select your income types"
    options %w[W2 1099 Business Investment]
    transition to: :business_details, if_rule: contains(:income_types, "Business")
    transition to: :summary
  end

  ask :business_details do
    type :integer
    question "How many businesses?"
    transition to: :summary
  end

  say :summary do
    text "Thank you! We'll be in touch."
  end
end

DSL Explained — verbs, types, and rules by example

The core flow verbs. ask and confirm collect an answer; say, header, btw and warning only display content.

ask
ask :first_name do
  type :string
  question "What's your first name?"
  transition to: :email
end
ask :filing_status do
  type :enum
  question "What is your filing status?"
  options %w[single married head_of_household]
  transition to: :spouse, if_rule: equals(:filing_status, "married")
  transition to: :summary
end
ask :dependents do
  type :integer
  question "How many dependents?"
  default 0
  transition to: :summary
end
say
say :welcome do
  text "Welcome! This takes about two minutes."
  transition to: :first_name
end
say :closing do
  text "Thanks — we'll email your estimate shortly."
end
header
header :income_section do
  text "Your income"
  transition to: :wages
end
header :about_you do
  text "A little about you"
  transition to: :first_name
end
btw
btw :privacy_note do
  text "We never share your data with third parties."
  transition to: :email
end
btw :pause_note do
  text "You can pause and come back any time."
  transition to: :first_name
end
warning
warning :deadline do
  text "The filing deadline is April 15."
  transition to: :summary
end
warning :documents do
  text "Have your W-2 and 1099 forms handy."
  transition to: :income_section
end
confirm
confirm :married do
  question "Are you married?"
  transition to: :spouse_income, if_rule: equals(:married, true)
  transition to: :summary
end
confirm :agree_terms do
  question "Do you agree to the terms?"
  transition to: :finish
end

The one LLM-powered verb. It reads a free-text answer and returns structured fields that pre-fill later questions. Runs server-side.

How an extraction is wired up

ask type :text

Precede it with an open-ended ask of type :text — that free-text answer is the raw material.

from :step_id

points at that question's id (list several to combine them).

prompt

instructs the model on what to pull out.

schema field: :type, …

declares exactly what you expect back; the output is validated against it. Types:

string, text, integer, decimal, currency, boolean, enum,
multi_enum, date, email, phone, array, hash

Extracted fields become answers keyed by the schema names, usable in later rules and skipped downstream via skip_if not_empty(:field). The Ruby verb is clarify; the JS widget accepts extract as a synonym.

# Step 1 — ask an open-ended question first
ask :business_description do
  type :text
  question "Describe your business in a few sentences."
  transition to: :business_info
end

# Step 2 — extract structured fields from that free text
# (the verb is `clarify`; the widget accepts `extract` as a synonym)
clarify :business_info do
  from :business_description
  prompt "Extract structured business information from the description."
  schema industry:          :string,
         entity_type:       :enum,
         employee_count:    :integer,
         estimated_revenue: :currency
  transition to: :summary
end
# Pull from more than one source step; pin the model + temperature
clarify :extracted do
  from :situation, :notes
  prompt "Pull out the filing details the client described."
  schema filing_status: :string,
         dependents:    :integer,
         state_filing:  :string
  model :claude_sonnet
  temperature 0.0
  transition to: :filing_status
end
# The extraction pre-fills answers…
clarify :extracted do
  from :describe
  prompt "Extract filing status and dependent count."
  schema filing_status: :enum, dependents: :integer
  transition to: :filing_status
end

# …and each downstream question is skipped when already known.
ask :filing_status do
  type :enum
  question "What is your filing status?"
  options %w[single married head_of_household]
  skip_if not_empty(:filing_status)
  transition to: :dependents
end
clarify :extracted do
  from :describe
  prompt "Extract the client's home state."
  schema state_filing: :string
  # server-side fallback if the model call fails (stripped from client JSON)
  fallback { |answers| "unknown" }
  transition to: :state_filing
end

The 11 answer types available to ask. Set one with `type :name`.

string
ask :field do
  type :string
  question "What's your first name?"
  transition to: :done
end
text
ask :field do
  type :text
  question "Anything else we should know?"
  transition to: :done
end
integer
ask :field do
  type :integer
  question "How many dependents?"
  default 0
  transition to: :done
end
decimal
ask :field do
  type :decimal
  question "Billable hours last month?"
  transition to: :done
end
currency
ask :field do
  type :currency
  question "Approximate annual revenue?"
  transition to: :done
end
boolean
ask :field do
  type :boolean
  question "Do you own your home?"
  transition to: :done
end
enum
ask :filing_status do
  type :enum
  question "Filing status?"
  options %w[single married head_of_household]
  transition to: :done
end
multi_enum
ask :income_types do
  type :multi_enum
  question "Which income types apply?"
  options %w[w2 self_employment investment rental]
  transition to: :done
end
date
ask :field do
  type :date
  question "Date of birth?"
  transition to: :done
end
email
ask :field do
  type :email
  question "What's your email address?"
  transition to: :done
end
phone
ask :field do
  type :phone
  question "Best phone number?"
  transition to: :done
end

enum and multi_enum draw their choices from `options`, given as a plain array of keys or a Hash of key => label.

enum
ask :filing_status do
  type :enum
  question "Filing status?"
  options %w[single married head_of_household]
  transition to: :done
end
ask :filing_status do
  type :enum
  question "Filing status?"
  # Hash form: value => human-readable label
  options single:            "Single",
          married_jointly:   "Married filing jointly",
          head_of_household: "Head of household"
  transition to: :done
end
multi_enum
ask :income_types do
  type :multi_enum
  question "Select all income types."
  options %w[w2 self_employment investment rental]
  transition to: :done
end
ask :income_types do
  type :multi_enum
  question "Select all income types."
  options w2:         "W-2 wages",
          business:   "Business income",
          investment: "Investments"
  # `contains` tests membership in a multi_enum answer
  transition to: :business_count, if_rule: contains(:income_types, "business")
  transition to: :done
end

Each step lists transitions in order; the first whose if_rule matches wins, and a rule-less transition is the fallback. Rules are serializable AST objects, never lambdas.

branch
ask :filing_status do
  type :enum
  question "Filing status?"
  options %w[single married]
  # first matching transition wins; the last (no rule) is the fallback
  transition to: :spouse, if_rule: equals(:filing_status, "married")
  transition to: :summary
end
equals
ask :state do
  type :string
  question "Which state?"
  transition to: :ca_rules, if_rule: equals(:state, "CA")
  transition to: :done
end
contains
ask :income_types do
  type :multi_enum
  question "Income types?"
  options %w[w2 business investment]
  transition to: :business_count, if_rule: contains(:income_types, "business")
  transition to: :done
end
compare
ask :dependents do
  type :integer
  question "How many dependents?"
  transition to: :large_family, if_rule: greater_than(:dependents, 3)
  transition to: :only_child,   if_rule: less_than(:dependents, 1)
  transition to: :done
end
not_empty
ask :notes do
  type :text
  question "Anything else we should know?"
  transition to: :review, if_rule: not_empty(:notes)
  transition to: :done
end
composite
ask :income_types do
  type :multi_enum
  question "Income types?"
  options %w[business investment rental]
  # composite rules nest atomic ones
  transition to: :complex_path,
    if_rule: all(contains(:income_types, "business"),
                 contains(:income_types, "rental"))
  transition to: :has_passive,
    if_rule: any(contains(:income_types, "investment"),
                 contains(:income_types, "rental"))
  transition to: :done
end

Declare a running total once with `accumulator`, then have steps contribute to it. The :price accumulator is the common lead-qualification use case; others work identically.

price
# Declare a running total once, at the top of the flow:
accumulator :price, type: :currency, default: 0

ask :filing_status do
  type :enum
  question "Filing status?"
  options %w[single married head_of_household]
  # `price` adds to the :price accumulator based on the chosen option
  price single: 150, married: 250, head_of_household: 200
  transition to: :done
end
per_unit
ask :rental_properties do
  type :integer
  question "How many rental properties?"
  price per_unit: 75          # $75 per property (rate × answer)
  transition to: :foreign
end

ask :foreign do
  type :boolean
  question "Any foreign income?"
  accumulate :price, flat: 300 # flat surcharge when answered
  transition to: :done
end
per_selection
accumulator :complexity, type: :integer, default: 0

ask :income_types do
  type :multi_enum
  question "Income types?"
  options %w[w2 business investment]
  price       per_selection: { w2: 0, business: 200, investment: 120 }
  accumulate :complexity, per_selection: { w2: 0, business: 2, investment: 1 }
  transition to: :done
end

Optional rendering hints for frontend adapters. Each data type has a sensible default, so `widget` is only needed to override it. Target :desktop (default), :mobile or :tty.

enum
ask :filing_status do
  type :enum
  question "Filing status?"
  options %w[single married head_of_household]
  widget type: :radio_group, columns: 2   # desktop (default target)
  widget target: :mobile, type: :dropdown # compact on phones
  transition to: :done
end
multi_enum
ask :income_types do
  type :multi_enum
  question "Income types?"
  options %w[w2 business investment rental]
  widget type: :checkbox_group, columns: 2
  widget target: :tty, type: :multi_select
  transition to: :done
end
slider
ask :satisfaction do
  type :integer
  question "How satisfied are you, 1 to 10?"
  widget type: :slider, min: 1, max: 10
  transition to: :done
end

Extra per-step options. (FlowEngine's visible_if is replaced by skip_if in Inquirex.)

default
ask :dependents do
  type :integer
  question "How many dependents?"
  default 0                    # pre-filled; the user can still change it
  transition to: :done
end
default_block
ask :state do
  type :string
  question "Which state do you file in?"
  default { |answers| "CA" }   # a Proc default can read prior answers
  transition to: :done
end
skip_if
ask :spouse_name do
  type :string
  question "Spouse's name?"
  # skip this step entirely unless the filer is married
  skip_if equals(:filing_status, "single")
  transition to: :done
end
compute
ask :preparer_fee do
  type :currency
  question "Estimated preparer fee"
  # runs server-side, not shown to the user, stripped from client JSON
  compute { |answers| answers.dig("dependents").to_i * 50 }
  transition to: :done
end