ORM Models — models.py
Nine tables model the whole domain: an Agent at the center owning messages, runs, tasks, tool calls, documents, chunks, and workflow traces — all cascade-deleted with the agent.
Purpose
backend/app/models.py defines every database table as a SQLAlchemy declarative model on database.Base. There is no other persistence definition anywhere in the codebase.
Entity relationship overview
Full column-level ERD and DDL notes: Database Schema.
The models
Agent — the aggregate root
| Column | Type | Notes |
|---|---|---|
id | Integer PK | |
name | String(100), required | display name, injected into prompts |
type | String(50), required | functional area (e.g. "Email Ops") |
description | Text, nullable | internal notes |
system_prompt | Text, required | the agent's instructions — see Prompts |
language / tone | String(50), defaults English / Professional | shape every LLM call |
enabled_tools | Text, default "" | comma-separated allow-list — see Tools |
created_at | DateTime(tz), server default now() |
Relationships — all with cascade="all, delete": messages, runs, tasks, tool_calls, documents, workflow_runs. Deleting an agent deletes its entire history.
The SQL cascade removes documents and document_chunks rows, but the corresponding vectors in Chroma are not deleted — there is no code path that calls documents_collection.delete. Orphaned vectors remain searchable in the collection (still scoped by agent_id, so they only affect a reused agent ID). Tracked in Known Issues.
Message — chat history
agent_id FK · role ("user" / "assistant", stored as String(20)) · content Text · created_at. Two rows are written per chat exchange (Request Lifecycle).
AgentRun — one chat execution
agent_id FK · input_text · output_text · latency_ms · status (default "success", set to "failed" on LLM errors) · error_message · created_at. Also owns tool_calls (cascade) via the optional ToolCall.agent_run_id back-reference.
Task — actionable items extracted from email
agent_id FK · title String(200) · description · priority (default "medium") · status (default "open"; API allows open | in_progress | completed) · due_date String(100) · source_type (default "email") · created_at.
Deadlines arrive from LLM analysis as free text ("tomorrow", "Friday", ISO dates). normalize_due_date() in the agents router keeps simple words, validates ISO dates (dropping past ones), and passes other text through — so the column stores what the model said, normalized, rather than forcing a lossy date parse.
ToolCall — the observability atom
agent_id FK · agent_run_id FK nullable · tool_name · tool_input · tool_output · success (String "true"/"false") · error_message · latency_ms · model_name · estimated_tokens (Integer) · estimated_cost (String(30)) · created_at.
The last three columns are the ones added by startup migrations. Why success/cost are strings: Design Decisions.
Document & DocumentChunk — RAG source of truth
Document:agent_idFK ·filename·file_type(default"txt") ·status(set to"indexed"on upload) ·chunks_count·created_at.DocumentChunk:document_idFK ·agent_idFK (denormalized for direct per-agent queries) ·chunk_text·chunk_index·vector_id(the Chroma ID:agent_{a}_document_{d}_chunk_{i}) ·created_at.
The vector_id is the bridge between SQLite and Chroma — see RAG Ingestion.
WorkflowRun & WorkflowStep — pipeline traces
WorkflowRun:agent_idFK ·workflow_name·input_text·final_output(JSON as Text) ·status(running→success/failed) ·quality_score(String — the reviewer's 0–1 score) ·error_message·created_at. Ownssteps(cascade).WorkflowStep:workflow_run_idFK ·step_name·input_data/output_data(JSON as Text) ·status·latency_ms·error_message·created_at.
Written exclusively by the email workflow endpoint today — see Workflows.
Conventions
- Every table has an autoincrement
idPK withindex=Trueand a timezone-awarecreated_atdefaulting tofunc.now()(server-side). - Foreign keys are
nullable=FalseexceptToolCall.agent_run_id(tool calls outside chats are legal and common). - JSON payloads (tool inputs/outputs, workflow step data, final outputs) are serialized with
json.dumpsintoTextcolumns — SQLite has no JSON column type worth using here.
Edge cases
- String booleans/floats (
ToolCall.success,estimated_cost,WorkflowRun.quality_score) require careful comparison/parsing — the dashboard usessafe_int/safe_floathelpers; do the same in new code. Task.agentuses a plainrelationship("Agent")withoutback_populates— navigable from task to agent, while the agent side uses its owntaskslist.- No uniqueness constraints beyond PKs — duplicate agent names, duplicate filenames per agent, etc., are all legal.
Future improvements
- Numeric columns for
estimated_cost/quality_score; Boolean forsuccess. - Delete Chroma vectors when documents/agents are deleted.
- Indexes on the FK columns used by dashboard aggregation.
Related files
- Database Schema — ERD with every column
- Schemas — the Pydantic mirror of these models
- Database Layer — engine and migrations