# AgentForms Architecture — Technical Reference
## System Design
AgentForms is a three-tier service:
| Component | Technology | Port | Responsibility |
|---|---|---|---|
| **Relay** | Flask + Gunicorn | 5060 | API surface, form logic, LLM client, auth |
| **Worker** | Python + Redis queue | — | Async action execution, email delivery |
| **LLM** | llama.cpp (Qwen3.5-4B) | 8086 | Form generation from natural language |
All three run in Docker Compose. SQLite is the default database with a Postgres migration planned.
## Agent Integration Pattern
The primary use case is an agent calling AgentForms on behalf of a user. The flow:
```
User → Agent → AgentForms API → Form Created → Form Published
↓
User fills form → Submission → Actions Fire
```
### Step 1: Agent creates a form
```python
import requests
# Agent asks user: "What kind of form do you need?"
# User says: "A client intake form for our consulting business"
resp = requests.post(
"https://agentforms.io/api/agent/build",
headers={"Authorization": "Bearer <api_key>"},
json={
"prompt": "Client intake form for consulting business: client name, company, email, phone, project description, budget range, timeline, existing contracts, referral source",
"form_type": "form"
}
)
site = resp.json()
token = site["token"] # Embed/publish token
fields = site["fields"] # Generated field config
```
### Step 2: Agent configures actions
```python
# Webhook to client's system
requests.post(
f"https://agentforms.io/api/sites/{site_id}/actions",
headers={"Authorization": "Bearer <api_key>"},
json={
"type": "webhook",
"config": {"url": "https://client-crm.com/api/lead"},
"execution_order": 1
}
)
# Email notification
requests.post(
f"https://agentforms.io/api/sites/{site_id}/actions",
headers={"Authorization": "Bearer <api_key>"},
json={
"type": "email",
"config": {
"recipients": ["sales@example.com"],
"subject": "New client inquiry: {{client_name}}"
},
"execution_order": 2
}
)
```
### Step 3: Agent returns embed URL to user
The user shares the embed link or the agent embeds it on their own site. No login required on the form-filler's side.
## Field Configuration Format
Every form is defined by a JSON array of field objects:
```json
[
{
"key": "client_name",
"type": "text",
"label": "Client Name",
"required": true,
"placeholder": "John Smith",
"validation": null,
"condition": null,
"calculation": null,
"step": 1
},
{
"key": "budget",
"type": "select",
"label": "Budget Range",
"required": true,
"options": ["Under $5K", "$5K-$15K", "$15K-$50K", "$50K+"],
"validation": null,
"condition": null,
"calculation": null,
"step": 1
}
]
```
### Supported Field Types
| Type | Description |
|---|---|
| `text` | Single-line text input |
| `email` | Email with validation |
| `phone` | Phone number input |
| `number` | Numeric input with min/max |
| `textarea` | Multi-line text |
| `select` | Dropdown with options |
| `radio` | Radio button group |
| `checkbox` | Single checkbox |
| `date` | Date picker |
| `url` | URL input |
| `computed` | Calculated field (read-only) |
| `file` | File upload (Phase 5) |
### Condition Operators
11 operators for conditional field visibility:
| Operator | Description | Example |
|---|---|---|
| `eq` | Equals | `{"field": "marital_status", "operator": "eq", "value": "married"}` |
| `neq` | Not equals | `{"field": "choice", "operator": "neq", "value": "none"}` |
| `filled` | Field has value | `{"field": "name", "operator": "filled"}` |
| `empty` | Field is empty | `{"field": "result", "operator": "empty"}` |
| `gt` | Greater than (numeric) | `{"field": "total", "operator": "gt", "value": "100"}` |
| `gte` | Greater than or equal | `{"field": "total", "operator": "gte", "value": "100"}` |
| `lt` | Less than (numeric) | `{"field": "age", "operator": "lt", "value": "18"}` |
| `lte` | Less than or equal | `{"field": "age", "operator": "lte", "value": "18"}` |
| `contains` | String contains | `{"field": "message", "operator": "contains", "value": "urgent"}` |
| `not_contains` | String does not contain | `{"field": "message", "operator": "not_contains", "value": "spam"}` |
| `in` | Value in list | `{"field": "product", "operator": "in", "value": "laptop,phone,tablet"}` |
| `not_in` | Value not in list | `{"field": "product", "operator": "not_in", "value": "laptop,phone,tablet"}` |
### Calculation Engine
Calculated fields use `{{key}}` template syntax:
```json
{
"key": "total",
"type": "computed",
"calculation": {
"formula": "{{price}} * {{quantity}}",
"round": 2
}
}
```
Formula engine supports basic arithmetic: `+`, `-`, `*`, `/`, parentheses.
## Action Pipeline
### Action Types
**Webhook**
```json
{
"type": "webhook",
"config": {
"url": "https://example.com/webhook",
"method": "POST",
"headers": {"Authorization": "Bearer ..."},
"body_template": "{{ submission_data }}"
}
}
```
**Email**
```json
{
"type": "email",
"config": {
"recipients": ["user@example.com"],
"subject": "New submission: {{client_name}}",
"body_template": "..."
}
}
```
**Log**
```json
{"type": "log", "config": {"message_template": "..."}}
```
**Redirect**
```json
{
"type": "redirect",
"config": {
"url": "https://example.com/thanks",
"delay": 3
}
}
```
**Document**
```json
{
"type": "document",
"config": {
"template_id": "contract_v1",
"output_format": "pdf"
}
}
```
### Execution Model
Actions execute in order of `execution_order` (ascending). Each action can be independently enabled/disabled. Failed actions don't block subsequent actions — they log and continue.
## Workflow Builder
The LLM client sends the user's prompt to a Qwen3.5-4B model running via llama.cpp. The model returns a structured JSON response that is parsed into `field_config`.
### Request Format
```python
requests.post(
f"{LLM_HOST}/v1/chat/completions",
json={
"model": "Qwen3.5-4B",
"messages": [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": user_prompt}
],
"temperature": 0.3,
"max_tokens": 2048
}
)
```
The system prompt instructs the model to generate valid field_config JSON with proper types, labels, and optional validation/condition/calculation.
### LLM Configuration
| Variable | Default | Description |
|---|---|---|
| `BUILDER_LLM_HOST` | `http://10.0.0.68:8086` | llama.cpp server address |
Overridable via Docker `.env` in the relay container.
## Database Schema
### Users
```sql
CREATE TABLE users (
id INTEGER PRIMARY KEY,
email TEXT UNIQUE,
password_hash TEXT,
name TEXT,
tier TEXT DEFAULT 'free',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
```
### Sites
```sql
CREATE TABLE sites (
id INTEGER PRIMARY KEY,
token TEXT UNIQUE,
name TEXT,
owner_email TEXT,
user_id INTEGER REFERENCES users(id),
field_config TEXT, -- JSON array
theme_config TEXT, -- JSON object
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
```
### Actions
```sql
CREATE TABLE actions (
id INTEGER PRIMARY KEY,
site_id INTEGER REFERENCES sites(id),
type TEXT, -- webhook/email/log/redirect/document
config TEXT, -- JSON object
trigger_event TEXT DEFAULT 'submission',
enabled BOOLEAN DEFAULT 1,
execution_order INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
```
### Submissions
```sql
CREATE TABLE submissions (
id INTEGER PRIMARY KEY,
site_id INTEGER REFERENCES sites(id),
data TEXT, -- JSON object of submitted fields
submitted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
```
### Form Versions
```sql
CREATE TABLE form_versions (
id INTEGER PRIMARY KEY,
site_id INTEGER REFERENCES sites(id),
field_config TEXT, -- Snapshot of field_config at this version
version_number INTEGER,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
```
## Security
- **API key auth** — `X-API-Key` or `Bearer` token for all agent-facing endpoints
- **CSRF protection** — Exempted for JSON API, required for HTML forms
- **Rate limiting** — Planned for Phase 5
- **Field sanitization** — `bleach` library for XSS prevention
- **PII encryption** — AES-256-GCM for sensitive fields (Phase 5)
## Deployment
### Docker Compose
```yaml
services:
relay:
build: .
ports: ["5060:5060"]
env_file: data/.env
volumes:
- ./app:/app/app
- ./data:/app/data
worker:
build: .
env_file: data/.env
volumes:
- ./app:/app/app
- ./data:/app/data
redis:
image: redis:7-alpine
```
### Environment Variables (data/.env)
| Variable | Purpose |
|---|---|
| `SECRET_KEY` | Flask session signing |
| `DATABASE_URL` | Database connection string |
| `BUILDER_LLM_HOST` | LLM server address |
| `RESEND_API_KEY` | Email provider |
| `STRIPE_*` | Billing integration |
## Error Handling
| Status | Meaning |
|---|---|
| `200` | Success |
| `201` | Created |
| `400` | Bad request / validation error |
| `401` | Unauthorized |
| `403` | Forbidden |
| `404` | Not found |
| `422` | Validation errors on submission |
| `500` | Internal server error |
Response format:
```json
{
"success": true/false,
"error": "message (if failed)",
"data": {...} // or "fields": [...] for submission errors
}
```