# 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`