# AgentForms Phase 3: Conditional Logic Engine — Specification
**Created:** 2026-06-11
**Status:** In Progress
**Author:** Vincent (AI) + Grepples
---
## 1. Overview
Phase 3 extends the existing conditional visibility system with grouped conditions (AND/OR),
conditional field behavior (required, default, validation), and recursive cascade evaluation.
The existing system already supports single conditions:
```json
{
"key": "phone",
"type": "phone",
"label": "Phone Number",
"condition": { "field": "contact_method", "operator": "eq", "value": "phone" }
}
```
Phase 3 adds:
```json
{
"key": "phone",
"type": "phone",
"label": "Phone Number",
"condition": {
"all": [
{ "field": "contact_method", "operator": "eq", "value": "phone" },
{ "field": "tier", "operator": "in", "value": ["pro", "enterprise"] }
]
},
"conditional_required": {
"field": "contact_method",
"operator": "eq",
"value": "phone"
},
"conditional_default": {
"field": "service",
"operator": "eq",
"value": "default_service"
}
}
```
---
## 2. Architecture
### 2.1 File Inventory
| File | Role | Phase 3 Changes |
|------|------|-----------------|
| `app/services/form_logic.py` | Server-side validation engine | Groups, cascade, conditional behavior |
| `app/static/embed.js` | Client-side SDK | Groups, cascade, conditional behavior |
| `app/templates/hosted_form.html` | Server-rendered hosted form | Pass condition groups to JS |
| `app/templates/edit_site.html` | Form builder UI | Condition group editor |
| `app/routes/api.py` | Submission API | Already calls `validate_submission()` |
| `app/routes/site.py` | Hosted form context | Already passes `field_config` |
| `app/models.py` | Schema/migrations | Migration for new condition format |
### 2.2 Processing Order
Both client and server must evaluate in this order:
1. **Resolve computed fields** — `{{key}}` template syntax, cascading resolution
2. **Evaluate conditions (recursive)** — visibility first, then conditional behavior
3. **Cascade hidden fields** — if A hides B, and B hides C, both are hidden
4. **Apply conditional behavior** — required, default, validation changes
5. **Validate visible fields** — standard validation, respecting conditional overrides
---
## 3. Schema Changes
### 3.1 Condition Groups
The `condition` field on each field definition now supports two formats:
**Legacy (single condition) — backward compatible:**
```json
"condition": { "field": "x", "operator": "eq", "value": "y" }
```
**New (grouped conditions):**
```json
"condition": {
"all": [
{ "field": "x", "operator": "eq", "value": "y" },
{ "field": "z", "operator": "filled" }
]
}
```
```json
"condition": {
"any": [
{ "field": "x", "operator": "eq", "value": "y" },
{ "field": "z", "operator": "eq", "value": "w" }
]
}
```
- `all` = AND — every sub-condition must be true
- `any` = OR — at least one sub-condition must be true
- Groups can nest: `all` can contain `any` which can contain `all` (recursive)
- Legacy single condition is auto-wrapped in `all: [condition]` during evaluation
### 3.2 Conditional Behavior
New optional fields on field definitions:
**`conditional_required`:**
```json
"conditional_required": { "field": "x", "operator": "eq", "value": "y" }
```
When the condition evaluates true, this field becomes required (overrides `required`).
**`conditional_default`:**
```json
"conditional_default": {
"condition": { "field": "x", "operator": "eq", "value": "y" },
"value": "auto-filled-value"
}
```
When the condition evaluates true, this field gets the specified default value.
**`conditional_validation`:**
```json
"conditional_validation": [
{
"condition": { "field": "x", "operator": "eq", "value": "y" },
"pattern": "^[A-Z]{2}\\d{4}$",
"errorMessage": "Must be state code + 4 digits"
}
]
```
Array of condition → validation rule pairs. When condition is true, merge these rules
with the base validation config.
### 3.3 Cascade Evaluation
Conditions can reference fields that are themselves conditional. Evaluation must cascade:
```
Field A: always visible
Field B: condition { field: "A", operator: "eq", value: "show_b" }
Field C: condition { field: "B", operator: "filled" }
```
When A ≠ "show_b", B is hidden. Since B is hidden, B has no value → C's condition is false → C hidden.
Implementation: evaluate all conditions, check if hidden, recurse until stable state.
Max 3 iterations to prevent infinite loops.
---
## 4. Server-Side Implementation
### 4.1 `app/services/form_logic.py`
**Current functions:**
- `evaluate_condition(condition, data)` — single condition
- `validate_field(field, value)` — single field validation
- `validate_submission(field_config, data)` — iterate all fields
- `compute_all_fields(field_config, data)` — computed field resolution
**Changes:**
1. **`evaluate_condition(condition, data)`** — extend to handle groups:
- If `condition` has `all` key → AND all sub-conditions
- If `condition` has `any` key → OR any sub-condition
- If `condition` has `field` key → legacy single condition (pass through)
- Recursive: each sub-condition calls `evaluate_condition` again
2. **`evaluate_all_conditions(field_config, data)`** — new function:
- Returns `{field_key: {visible: bool, required: bool, default: str, validation: dict}}`
- Processes all fields, resolves conditions + conditional behavior
- Handles cascade (max 3 iterations)
3. **`validate_field(field, value, overrides=None)`** — add `overrides` param:
- `overrides` dict can contain `required`, `pattern`, `minLength`, `maxLength`, `min`, `max`
- Overrides merge with base field config before validation
4. **`validate_submission(field_config, data)`** — update to use `evaluate_all_conditions()`:
- Get visibility + behavior for all fields first
- Only validate visible fields with their conditional overrides
### 4.2 `app/routes/api.py`
No changes needed — `validate_submission()` already called in submit pipeline.
The internal refactoring in `form_logic.py` is transparent to the API.
### 4.3 `app/routes/site.py`
No changes needed — `_get_hosted_form_context()` already passes `field_config` to the template.
---
## 5. Client-Side Implementation
### 5.1 `app/static/embed.js`
**Current functions:**
- `evaluateCondition(condition, formData)` — single condition
- `updateFieldVisibility(formData)` — iterate fields, toggle display
- `wireConditionListeners()` — add change/input listeners on dependent fields
**Changes:**
1. **`evaluateCondition(condition, formData)`** — same group logic as server:
- `all` → AND, `any` → OR, `field` → legacy
- Recursive for nested groups
2. **`evaluateAllConditions(fields, formData)`** — new function:
- Returns condition state per field (mirrors server `evaluate_all_conditions`)
- Cascade: iterate up to 3 times until stable
3. **`applyConditionalBehavior(field, state)`** — new function:
- If `conditional_required` → toggle `required` attribute
- If `conditional_default` → set value + trigger change events
- If `conditional_validation` → update validation config for this field
4. **`updateFieldVisibility(formData)`** — update to call `evaluateAllConditions()`:
- Apply visibility + behavior in one pass
5. **`wireConditionListeners()`** — update to track all dependent fields in groups:
- Extract all referenced fields from condition groups (recursive)
- Wire listeners on each
### 5.2 `app/templates/hosted_form.html`
No template changes needed. The field_config JSON is passed to `<script>` and
evaluated client-side. Server-side rendering already handles conditional visibility
via the Jinja2 loop (existing behavior).
---
## 6. Form Builder UI
### 6.1 `app/templates/edit_site.html`
**Current UI:** Single condition per field (depends-on field, operator, value).
**New UI sections:**
**Condition Groups Editor:**
- Replace single condition inputs with:
- Toggle: "Single Condition" / "Condition Group"
- Group type: AND / OR (radio buttons)
- "Add Condition" button → appends condition row
- Each row: field selector, operator, value input
- Delete button per row
- "Add Nested Group" button for nesting AND/OR
**Conditional Behavior Editor:**
- Collapsible sections per field:
- "Conditional Required" — field + operator + value
- "Conditional Default" — condition + value
- "Conditional Validation" — condition + pattern + error message
**JavaScript functions to update:**
- `getFieldFromRow()` — serialize condition groups + conditional behavior
- `addField()` — include new UI sections
- `updateAdvancedSections()` — show/hide based on field type
---
## 7. Migration
### 7.1 `app/models.py` — `migrate_phase3_conditions()`
- Run on first start after Phase 3 deploy
- Wrap legacy single conditions in `all: [condition]` for consistency
- Add empty `conditional_required`, `conditional_default`, `conditional_validation` defaults
- No data loss — legacy format is preserved and auto-wrapped at evaluation time
**Note:** Auto-wrapping at runtime is preferred over in-place DB migration.
The evaluator handles both formats transparently.
---
## 8. Testing Strategy
### 8.1 Server-Side Tests
```python
# Test condition groups
assert evaluate_condition({"all": [c1, c2]}, data) == True
assert evaluate_condition({"any": [c1, c2]}, data) == True
assert evaluate_condition({"all": [{"any": [c1, c2]}]}, data) == True # nested
# Test cascade
field_config = [A(always visible), B(if A=show), C(if B=filled)]
state = evaluate_all_conditions(field_config, {"A": "other"})
assert state["B"]["visible"] == False
assert state["C"]["visible"] == False
# Test conditional required
field = {"key": "x", "conditional_required": {"field": "y", "operator": "eq", "value": "1"}}
assert validate_field(field, "", overrides={"required": True}) != None
```
### 8.2 Client-Side Tests
- Test `evaluateCondition` with groups in browser console
- Test cascade in hosted form: toggle dependent field, verify all descendants hide
- Test conditional required: toggle condition, verify `required` attribute appears
- Test conditional default: toggle condition, verify value auto-fills
### 8.3 Integration Tests
```bash
# Test API submission with conditional fields
curl -X POST "http://localhost:5060/api/submit?token=XXX" \
-d "visible_field=value&hidden_field=should_be_ignored"
# Hidden fields should be skipped in validation
# Conditional required should enforce when condition is met
```
---
## 9. Implementation Order
1. **`form_logic.py`** — `evaluate_condition` groups + `evaluate_all_conditions` + cascade
2. **`embed.js`** — `evaluateCondition` groups + `evaluateAllConditions` + `applyConditionalBehavior`
3. **Test server-side** — validate_submission with groups
4. **Test client-side** — hosted form with groups
5. **`edit_site.html`** — condition group editor UI
6. **`edit_site.html`** — conditional behavior editor UI
7. **End-to-end test** — create form in builder, test hosted form behavior
---
## 10. Backward Compatibility
- Legacy single condition format works unchanged (auto-wrapped at evaluation time)
- Fields without groups/conditional behavior behave identically to current version
- No DB schema changes required — all new fields are optional JSON keys
- Existing forms work without modification
## 11. Existing System Capabilities (Anti-Specification Reference)
### 11.1 Current Field Types
`text`, `email`, `textarea`, `number`, `phone`, `select`, `radio`, `checkbox`, `file`, `computed`
### 11.2 Current Operators (12 total)
`eq`, `neq`, `contains`, `not_contains`, `gt`, `lt`, `gte`, `lte`, `in`, `not_in`, `filled`, `empty`
### 11.3 Current Field Config Keys
`key`, `name`, `type`, `label`, `required`, `placeholder`, `options`, `default`,
`min`, `max`, `minLength`, `maxLength`, `pattern`, `errorMessage`, `condition`,
`calculation`, `step`
### 11.4 Current Validation Pipeline
1. Honeypot check → silent 200 drop
2. Server-side validation → 422 with per-field errors
3. Computed fields → merge into data
4. Tier enforcement → 429 if limit reached
5. Email notification → owner
6. Webhook delivery → async background thread
7. Document generation → async background thread
8. Return 201 with submission_id
### 11.5 Key Files
- Server validation: `app/services/form_logic.py`
- Client SDK: `app/static/embed.js`
- Hosted form: `app/templates/hosted_form.html`
- Form builder: `app/templates/edit_site.html`
- Submission API: `app/routes/api.py`
- Schema/migrations: `app/models.py`