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.
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.
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
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
equals(:step_id, "value") — exact matchcontains(:step_id, "value") — array includes valuegreater_than(:step_id, n) / less_than(:step_id, n)not_empty(:step_id)all(rule1, rule2) / any(rule1, rule2) — combine rules
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 stringstext / markdown_text / html — the body, written inline as a stringheaders — 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
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
The core flow verbs. ask and confirm collect an answer; say, header, btw and warning only display content.
askask :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
saysay :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
headerheader :income_section do
text "Your income"
transition to: :wages
end
header :about_you do
text "A little about you"
transition to: :first_name
end
btwbtw :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
warningwarning :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
confirmconfirm :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.
ask type :textPrecede it with an open-ended ask of
type :text — that free-text answer is the raw material.
from :step_idpoints at that question's id (list several to combine them).
promptinstructs 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`.
stringask :field do
type :string
question "What's your first name?"
transition to: :done
end
textask :field do
type :text
question "Anything else we should know?"
transition to: :done
end
integerask :field do
type :integer
question "How many dependents?"
default 0
transition to: :done
end
decimalask :field do
type :decimal
question "Billable hours last month?"
transition to: :done
end
currencyask :field do
type :currency
question "Approximate annual revenue?"
transition to: :done
end
booleanask :field do
type :boolean
question "Do you own your home?"
transition to: :done
end
enumask :filing_status do
type :enum
question "Filing status?"
options %w[single married head_of_household]
transition to: :done
end
multi_enumask :income_types do
type :multi_enum
question "Which income types apply?"
options %w[w2 self_employment investment rental]
transition to: :done
end
dateask :field do
type :date
question "Date of birth?"
transition to: :done
end
emailask :field do
type :email
question "What's your email address?"
transition to: :done
end
phoneask :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.
enumask :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_enumask :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.
branchask :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
equalsask :state do
type :string
question "Which state?"
transition to: :ca_rules, if_rule: equals(:state, "CA")
transition to: :done
end
containsask :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
compareask :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_emptyask :notes do
type :text
question "Anything else we should know?"
transition to: :review, if_rule: not_empty(:notes)
transition to: :done
end
compositeask :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_unitask :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_selectionaccumulator :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.
enumask :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_enumask :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
sliderask :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.)
defaultask :dependents do
type :integer
question "How many dependents?"
default 0 # pre-filled; the user can still change it
transition to: :done
end
default_blockask :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_ifask :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
computeask :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