June 30, 2025 · Piyush Ranjan Mishra
Building a Configuration-Driven Form Engine for Multi-Workflow Data Input
At Revfx I designed a configuration-driven dynamic form system to handle data input across multiple AI workflows — lead qualification, hiring-signal review, account research, each with a different shape of data to collect, and new workflows added regularly enough that hardcoding a form per workflow would have meant a constant stream of small frontend PRs just to keep up. The goal was a system where a new workflow’s form could be defined as configuration, not code.
The core abstraction: fields, not forms
The system didn’t model “forms” as the primary unit — it modeled field definitions as the primary unit, and a form was just an ordered list of field references plus some layout metadata. Each field definition specified its type (text, select, multi-select, structured object, computed/derived), validation rules, and how it mapped to the underlying data model. This meant a new workflow’s “form” was mostly assembling existing field definitions in a new order, with maybe one or two genuinely new field types needed — not building a bespoke form component every time.
interface FieldDefinition {
id: string;
type: "text" | "select" | "multiSelect" | "structured" | "computed";
label: string;
validation?: ValidationRule[];
dependsOn?: string[]; // conditional visibility/computation
}
Conditional logic is where configuration-driven systems usually break
The hard part of any dynamic form system isn’t rendering fields from config — it’s conditional behavior: field B only appears if field A has a certain value, field C’s options depend on field A and B together, a computed field derives from three others. A naive approach hardcodes this conditional logic per-workflow, which defeats the purpose of the system (you’re back to writing code per workflow, just scattered across a config file instead of a component file). The fix was making dependsOn a first-class part of the field definition, with a small expression evaluator for visibility and computed-value rules, so conditional logic was itself data, not code. This is the single decision that determined whether the system actually delivered on “add a workflow without a deploy,” or just moved the same complexity somewhere less visible.
Validation needed to live in one place, not two
An early mistake: validation rules initially lived partly in the field config (for client-side UX) and partly in backend logic (for actual data integrity), duplicated by hand. These drifted apart within weeks — a validation rule updated on one side and forgotten on the other, producing forms that let through data the backend then rejected with a confusing error, or worse, silently accepted data the frontend meant to block. The fix was making the field configuration the single source of truth, shared (via a serializable rule format) between frontend validation and a backend validator that consumed the same rule definitions. More upfront design work, but it eliminated an entire category of “the form says this is fine but the API disagrees” bugs.
Configuration needs versioning, or old submissions become unreadable
Once workflows and their forms could change without a deploy, a real problem emerged: what happens to data submitted under an older version of a form’s configuration, once that configuration changes? If field definitions aren’t versioned, rendering historical submissions against the current config can silently misinterpret old data — a field that used to be a single-select rendered against a config where it’s now multi-select, for instance. The fix was storing a config version alongside every submission, and rendering historical data against the config version it was actually submitted under, not the live one. This is easy to skip early on and expensive to retrofit once you have real historical data depending on it — worth building in from day one if you’re doing anything like this.
Where I’d draw the line on “configuration-driven”
Not everything belongs in configuration. Field definitions, layout, validation, and conditional visibility — yes, that’s genuinely reusable structure. Business logic that determines what happens after submission (which downstream workflow triggers, what gets written where) stayed in code, not configuration, because that logic tends to have edge cases and side effects that are much easier to reason about, test, and debug as actual code than as an ever-growing pile of configuration rules trying to express arbitrary logic. The line I used: if it’s about what data looks like and how it’s collected, configuration. If it’s about what the system does with that data, code.