Skip to main content
If you have questions during your migration, we can help! See Get Help for more information.
Refer the v3.0 Changelog for the full list of changes.
Want to migrate automatically? Jump to Migrate with AI for a prompt you can paste into Claude, Cursor or any coding agent.

Installing Agno v3

If you are already using Agno, you can upgrade to v3 by running:

Migrating your Agno DB

The built-in migration makes two schema changes:
  1. Session runs move to their own table. In v2, every session row held its full run history as a single JSON blob in the runs column. In v3, each run is its own row in a dedicated runs table (agno_runs by default), which removes the write amplification and unbounded row growth of the blob design.
  2. On the SQL adapters, a user_id column (with index) is added to the evals, components, knowledge, schedules, schedule-runs and metrics tables, for user isolation. The metrics unique key changes from (date, aggregation_period) to include user_id. Document and KV backends need no schema change here: per-user scoping on those comes from the v3 write path, so on them the migration only moves the runs.
One command applies both:
migrate_to_v3.py
On the async adapters (AsyncPostgresDb, AsyncMySQLDb, AsyncSqliteDb, AsyncMongoDb) get_runs and the cleanup method are coroutines — await them: runs = asyncio.run(db.get_runs(limit=5)) and asyncio.run(db.cleanup_legacy_runs_column(force=True)) (on AsyncMongoDb the method is cleanup_legacy_runs_field).
Vector databases are migrated separately. If you use per-user knowledge with a vector table created before v3, run the matching script from libs/agno/migrations/v2_to_v3 (migrate_sql_vectordbs.py, migrate_field_vectordbs.py or migrate_sentinel_vectordbs.py, depending on your vector store) to add user_id scoping to existing collections. On the schema-based stores (PgVector, SingleStore, LanceDB, Milvus, ClickHouse, Redis, Cassandra, Couchbase) an un-migrated table raises a ValueError on user-scoped searches instead of returning empty results. Schemaless stores (Qdrant, Pinecone, Upstash, Chroma, MongoDB, OpenSearch, SurrealDB) need no migration: pre-v3 documents stay visible to every user as shared. Notes:
  • The migration is non-destructive and idempotent: the legacy runs column is preserved as a backup, and re-running the migration never duplicates runs.
  • Reads keep working before, during and after the migration. Sessions merge the runs table with any legacy blob, so an un-migrated session still shows its history.
  • cleanup_legacy_runs_column() refuses to run while legacy data is present unless you pass force=True. Only pass force=True after Step 2 passes. Cleanup permanently deletes the blob, which is the only copy of your history if the migration did not actually copy it.
  • Supported everywhere sessions are stored: Postgres, MySQL, SQLite, SingleStore, MongoDB, Redis, Valkey, Firestore, DynamoDB, SurrealDB, JSON, and GCS JSON, plus the async Postgres, MySQL, SQLite and MongoDB adapters.
For the full storage design and per-adapter details, see the v3 storage migration guide in the repository.

Migrating your Agno code

Each section covers one breaking change, with before and after examples.

1. Sessions and runs (denormalization)

Reading sessions is unchanged. session.runs is still populated, now from the runs table:
v3_sessions.py
If you queried the runs column of the sessions table directly (SQL, dashboards, exports), point those queries at the runs table instead. After cleanup the column no longer exists:

2. Workflow HITL: flat kwargs → HumanReview

Workflow primitives no longer accept flat HITL kwargs. All human-in-the-loop configuration lives in one HumanReview object. This is how it looked in v2:
v2_hitl.py
This is how it looks in v3:
v3_hitl.py
Field mapping: every flat kwarg keeps its name inside HumanReview, except hitl_max_retriesmax_retries and hitl_timeouttimeout. This applies to Step, Steps, Loop, Condition and Router.

3. Removed and renamed parameters

These deprecated parameters have been removed. Update them to their v3 names: Agent and Team constructors:
v3_agent_params.py
continue_run / acontinue_run: the updated_tools parameter is removed. Pass requirements (a list of RunRequirement, available on the paused run output) instead of a modified ToolExecution list:
v3_continue_run.py
JWT middleware and authorization_config: secret_key is removed. Use verification_keys, which takes a list:
v3_jwt.py
MCPToolbox: auth_tokens and auth_headers are removed. Use auth_token_getters (same shape: a mapping of auth source names to token callables).

4. Reasoning requires an explicit model

The reasoning=True shortcut has been removed. Pass a native reasoning model explicitly:
v2_reasoning.py
v3_reasoning.py

5. The Workflow constructor is keyword-only

Workflow no longer accepts positional arguments:
v2_workflow.py
v3_workflow.py
Team is unchanged: Team([agent_1, agent_2]) still works. The keyword form Team(members=[...]) is preferred for clarity but is not required.

6. User isolation: user_id across the platform

With user_isolation enabled on AgentOS, data is now scoped per user across memories, knowledge, evals, metrics, schedules and vector databases, in addition to sessions. What this means for your code and data:
  • user_id columns were added to the schedules, schedule-runs and evals tables; the built-in migration handles this.
  • Metrics aggregate per user: the unique key changed from (date, aggregation_period) to (user_id, date, aggregation_period). Deployments without isolation see the same single-row-per-date shape as before; sessions without a user_id aggregate into a shared bucket.
  • Vector database collections created before v3 have no per-user scoping. On schema-based stores, searching them with a user_id raises a ValueError telling you to run the vector database migration — an un-migrated table fails loudly instead of silently returning empty results. On schemaless stores (Qdrant, Pinecone, Upstash, Chroma, MongoDB, OpenSearch, SurrealDB) pre-v3 documents are simply treated as shared.

7. Background execution and durable queues

background=True on AgentOS is rebuilt around a durable job queue. In v2 it spawned an unbounded asyncio.create_task, and a process death silently lost every waiting and in-flight run. In v3:
  • Accepted requests are committed rows that survive crashes, restarts and deploys; any replica’s worker can execute them.
  • Runs are bounded by a concurrency cap; excess submissions wait in the queue in pending status instead of overloading the process.
  • Every run can be watched (stream=true tails), resumed after a disconnect (/resume) and cancelled from any replica.
  • Idempotency-Key headers deduplicate resubmissions.
  • Redis is optional coordination (live event streams, cross-replica cancellation), never truth. A Redis fault degrades the live view; it cannot lose or corrupt a run.
Breaking implications: background execution requires a db on the agent (enforced with a 400), run status now transitions pending → running → completed (poll GET /agents/{id}/runs/{run_id} for the terminal state), and external framework agents (LangGraph, Claude, etc.) stream inline, so their runs are not resumable.

8. Culture feature removed

The experimental culture feature (enable_agentic_culture, add_culture_to_context, CulturalKnowledge, the agno_culture table) has been removed. Remove any references; if you need shared knowledge across users, use Knowledge instead.

9. Entity memory is isolated per user

If you use EntityMemoryStore with namespace="user", your existing rows are shared across users and must be re-keyed. In v2 the row key carried no user component, so two users who recorded an entity with the same name and type wrote to the same physical row: one user’s facts overwrote the other’s and then appeared in their prompt context. In v3 the key embeds a digest of the user_id. Global and custom namespaces are unchanged. Pre-v3 rows are re-keyed by the migration, not at runtime — until you run it, reads still match the old shared rows. The re-key is part of the v3.0.0 migration, so MigrationManager(db).up() (or POST /databases/all/migrate) covers it along with everything else:
v3_rekey_entities.py
This migration cannot be reversed. down() refuses the re-key, because the pre-v3 key is shared across users and restoring it would collide the rows again. Back up the learnings table before running it.
Reading the report: rekeyed moved to the owner’s key, and keyed was already correct. merged is expected rather than an error — if the upgraded application wrote to the user-scoped key before the migration ran, the entity exists in two rows and they are folded together, with the newer row winning. conflicts and failed need an operator: resolve them, then re-run the helper. Rows whose stored content records a different user than their owner column held two users’ data before the fix and cannot be separated. The migration moves these to the quarantined_user namespace instead of deleting them: the content is preserved and entity memory stops reading it. They remain listed and mutable through the /learnings API for whichever user the owner column names. To delete them instead — along with every row that has no owner — and let entity memory re-capture from conversation, pass purge_unrecoverable=True. Two API changes come with it:
  • delete / adelete take a keyword-only user_id and refuse namespace="user" deletes without it. Previously any caller could delete another user’s entity by name.
  • get / aget require a user_id in that namespace instead of returning an arbitrary user’s row.

10. Smaller changes

  • AgentOS metadata routes: GET /models was removed (its data moved into GET /config under available_models), and GET / is now a minimal landing response. GET /info is the single unauthenticated metadata endpoint.
  • Toolkits have an id, used by AgentOS to reference tools stably.
  • Schedule provenance columns: the schedules table gains eight nullable columns (managed_by, target_type, target_id, created_by_run_id, created_by_session_id, updated_by_run_id, updated_by_session_id, disabled_reason), added by the v3.0.0 migration on SQLite and PostgreSQL. Existing rows keep NULL provenance and no data is rewritten, so this needs no action beyond running the migration. If you query the schedules table directly with SELECT *, expect the extra columns.
  • update_schedule is restricted to a column allow-list: it now writes only name, description, method, endpoint, payload, cron_expr, timezone, timeout_seconds, max_retries, retry_delay_seconds, enabled, next_run_at and disabled_reason. Passing a provenance column raises a ValueError instead of silently repointing the row’s owner or target. user_id is not an update field either: it scopes the update to that owner, so an update passing the wrong user_id matches nothing.
  • Removed toolkit methods: DuckDuckGoTools.duckduckgo_search -> web_search and duckduckgo_news -> search_news; FileTools.check_escape -> Toolkit._check_path; PgVector.enable_prefix_matching removed (dead helper); BrightDataTools.get_screenshot no longer takes output_path.
  • Removed learn aliases: MemoriesConfig -> UserMemoryConfig, MemoriesStore -> UserMemoryStore, Decision -> DecisionLog.
  • Eval result files: store_result_in_file’s eval_id parameter is now run_id, and {eval_id} is no longer accepted in file_path_to_save_results templates — use {run_id}. POST /eval-runs returns the id the row was stored under.
  • Workspace refuses credential files by default: env files and conventional credential paths (*.pem, .ssh, .aws, credentials.json, *.tfvars, …) are excluded, so an agent that reads one starts getting a refusal. Re-allow specific paths with Workspace(".", allow_paths=["config/credentials.json"]). Committed templates such as .env.example become readable.
  • Studio memory forms: enable_agentic_memory and memory_manager_id are gone from the Studio create/edit forms. Use learning_name (a registry machine) or enable_learning=True. The Agent/Team constructor parameters are unchanged, so stored configs keep rehydrating.
  • SQLite uses WAL: SqliteDb/AsyncSqliteDb connect in WAL journal mode, which creates -wal and -shm sidecar files next to the database. Copy or back up all three together.
  • MultiMCPTools removed: use one MCPTools per server. The allow_partial_failure parameter is gone with it.
  • Knowledge insert API: add_content -> insert(), add_content_async -> ainsert(), add_contents_async -> ainsert_many().
  • Flat Google tool modules removed: import from agno.tools.google.* instead of agno.tools.gmail, agno.tools.googlesheets, agno.tools.googlecalendar, agno.tools.google_maps, agno.tools.google_drive, agno.tools.google_bigquery. Their parameters changed too: creds_path -> credentials_path, auth_port -> oauth_port.
  • Other toolkit renames: SeltzTools.max_documents -> max_results (older seltz SDKs still work through a fallback; seltz>=1.2.0 is needed for the scope, domain and date filters); BrandfetchTools drops async_tools; StudioTool -> StudioTools; GDriveContextProvider -> GoogleDriveContextProvider.
  • AgentOS MCP config: AgentOS(enable_mcp_server=..., mcp_config=...) -> mcp_server= (a bool or MCPServerConfig).
  • Removed model APIs: the agno.models.metrics module and its Metrics alias are gone — use agno.metrics / RunMetrics. Model.classify_error -> ModelProviderError.classify(error).
  • LanceDb.use_tantivy is removed; passing it now raises a TypeError.
  • Pagination is validated: page without limit, or page < 1, now raises a ValueError instead of being ignored.
  • Schedule names are unique per user: the unique key becomes (user_id, name). If duplicate names already exist, the v3.0.0 migration aborts rather than stamping itself done — resolve the duplicates and re-run.
  • Mistral requires mistralai>=2.0.0: the v1 compatibility layer is gone. Upgrade with pip install -U "agno[mistral]".
  • Cerebras default model: Cerebras and CerebrasOpenAI now default to gpt-oss-120b instead of llama-4-scout-17b-16e-instruct. Pin the old id explicitly if you depend on it.
  • agno[postgres] installs a working driver: the extra previously installed psycopg-binary only, so PostgresDb failed with ModuleNotFoundError: No module named 'sqlalchemy'. It now pulls psycopg and sqlalchemy; you can drop any manual pins you added to work around it.

Migrate with a Coding Agent

Paste the prompt below into Claude, Cursor, or any coding agent with access to your repository. It applies the mechanical changes and flags everything that needs your judgment.
Copy this prompt
The prompt deliberately refuses to run the destructive cleanup step. Keep it that way: verify your migrated history in the AgentOS UI before reclaiming the legacy storage.