Skip to content
2 min read · 473 words

Guide: Add a Migration

Worked example: adding a retry_count column to tool_calls. Two edits — the model and the migration dict — plus a verification step most people skip.

Background: Migrations — why the platform uses create_all + a startup column-adder instead of Alembic.

Step 1 — Add the column to the model

python
retry_count = Column(Integer, nullable=True)

The mechanism can only ADD COLUMN. The column must be nullable or defaulted — SQLite cannot add a NOT-NULL column without a default to a table with rows. Renames, type changes, drops, and backfills are out of scope (decision flowchart).

Step 2 — Add the matching migration entry

python
tool_call_columns = {
    "model_name": "VARCHAR(100)",
    "estimated_tokens": "INTEGER",
    "estimated_cost": "VARCHAR(30)",
    "retry_count": "INTEGER",          # ← new
}

The SQL type must match the model's column type (IntegerINTEGER, String(30)VARCHAR(30), TextTEXT).

Different table? Add a parallel dict + PRAGMA/ALTER block for that table in the same function — the existing block is the template:

python
task_columns = {"archived": "VARCHAR(10)"}
with engine.begin() as connection:
    existing = {row[1] for row in connection.execute(text("PRAGMA table_info(tasks)"))}
    for name, type_ in task_columns.items():
        if name not in existing:
            connection.execute(text(f"ALTER TABLE tasks ADD COLUMN {name} {type_}"))

Step 3 — Expose it (if the API should return it)

python
retry_count: int | None = None

And mirror it in any frontend type that renders tool calls (typing discipline).

Step 4 — Verify against an existing database

This is the step that catches real breakage — fresh DBs always work (they get the column from create_all); it's long-lived dev DBs that break:

bash
cd backend/app
uv run uvicorn main:app --reload --port 8001     # boot against your existing agentops.db
 
sqlite3 agentops.db "PRAGMA table_info(tool_calls);"   # confirm retry_count appears
curl http://localhost:8001/agents/1/tool-calls | jq '.[0]'  # no 'no such column' error

If you see sqlite3.OperationalError: no such column, the migration entry is missing or misspelled — the failure signature.

Step 5 — Document

Update ORM Models, the Schema ERD, and the schema page if API-visible. Column semantics belong in the docs the moment they exist — undocumented columns rot fastest.

New table instead of new column?

Just define the model — create_all creates it on next boot, no migration entry needed. Remember cascade="all, delete" on the agent relationship if the agent owns it, and an index=True primary key, matching the house schema conventions.