Migrating to Agno v3.0
Guide to migrate your Agno applications from v2 to v3.
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:
pip install -U agnoMigrating your Agno DB
The built-in migration makes two schema changes:
- Session runs move to their own table. In v2, every session row held its
full run history as a single JSON blob in the
runscolumn. In v3, each run is its own row in a dedicated runs table (agno_runsby default), which removes the write amplification and unbounded row growth of the blob design. - On the SQL adapters, a
user_idcolumn (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 includeuser_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.
Take a restorable backup and stop application writers before applying migrations. The v3 learning re-key also runs when applicable; it uses a non-transactional read/copy/delete sequence and must finish before the upgraded application accepts writes.
Run the migration with the same database configuration as your application:
import asyncio
from agno.db.postgres import PostgresDb # or SqliteDb, MongoDb, RedisDb, ...
from agno.db.migrations.manager import MigrationManager
db = PostgresDb(db_url="postgresql+psycopg://...")
# Step 1: run all v3 migrations (runs table + user_id columns)
asyncio.run(MigrationManager(db).up())
# Step 2: inspect a sample. This does not prove the migration is complete.
print(db.get_runs(limit=5))Before migrating, take a restorable database backup. Before any cleanup, compare all relevant legacy session/run identities and their history with the new runs storage, including sessions outside the sample. An empty database may legitimately have no runs. A nonempty sample can contain unrelated new runs and is not proof that every legacy run was copied.
Cleanup is a separate, optional operator action after that complete verification.
SQL adapters expose cleanup_legacy_runs_column; document and key-value adapters
expose cleanup_legacy_runs_field. Passing force=True bypasses their legacy-data
check and permanently removes that backup history. Keep the legacy data until
you have verified completeness and can restore the external backup.
On async adapters (AsyncPostgresDb, AsyncMySQLDb, AsyncSqliteDb, and
AsyncMongoDb), await get_runs and any separately approved cleanup operation.
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
runscolumn 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 passforce=True. Only considerforce=Trueafter a restorable backup and complete history verification. 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:
session = agent.get_session(session_id="s1")
session.runs # still works, loaded from the runs table
# New: fetch runs directly, without loading the whole session
runs = db.get_runs(session_id="s1")
run = db.get_run(run_id="...")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:
SELECT run_id, run_data FROM agno_runs WHERE session_id = 's1' ORDER BY run_index;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:
from agno.workflow.step import Step
step = Step(
name="deploy",
executor=deploy,
requires_confirmation=True,
confirmation_message="Deploy to production?",
)This is how it looks in v3:
from agno.workflow.step import Step
from agno.workflow.types import HumanReview
step = Step(
name="deploy",
executor=deploy,
human_review=HumanReview(
requires_confirmation=True,
confirmation_message="Deploy to production?",
),
)Field mapping: every flat kwarg keeps its name inside HumanReview, except
hitl_max_retries → max_retries and hitl_timeout → timeout. 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:
| v2 (removed) | v3 |
|---|---|
enable_user_memories | update_memory_on_run |
search_session_history | search_past_sessions |
num_history_sessions | num_past_sessions_to_search |
num_past_session_runs | num_past_session_runs_in_search |
agent = Agent(
update_memory_on_run=True,
search_past_sessions=True,
num_past_sessions_to_search=3,
)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:
run = agent.run("...") # pauses for confirmation
for requirement in run.requirements:
requirement.confirm()
agent.continue_run(run_id=run.run_id, requirements=run.requirements)JWT middleware and authorization_config: secret_key is removed. Use
verification_keys, which takes a list:
JWTMiddleware(verification_keys=["your-key"]) # was: secret_key="your-key"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:
agent = Agent(model=OpenAIResponses(id="gpt-5.5"), reasoning=True)agent = Agent(
model=OpenAIResponses(id="gpt-5.5"),
reasoning_model=OpenAIResponses(id="o4-mini"),
)5. The Workflow constructor is keyword-only
Workflow no longer accepts positional arguments:
workflow = Workflow("my-workflow", steps=[...])workflow = Workflow(id="my-workflow", steps=[...])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_idcolumns 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 auser_idaggregate into a shared bucket. - Vector database collections created before v3 have no per-user scoping. On
schema-based stores, searching them with a
user_idraises aValueErrortelling 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 runs are now capped at 32 per replica. In v2 each background=True
submission spawned an unbounded asyncio.create_task; in v3 runs beyond the
cap wait as PENDING instead of overloading the process. Raise or disable the
cap with AgentOS(queue=QueueConfig(max_concurrency=...)) or
AGNO_BACKGROUND_MAX_CONCURRENCY.
The rest of the queue is opt-in through QueueConfig:
durable=True: accepted requests become committed rows that survive crashes, restarts and deploys; any replica's worker can execute them. EnablesIdempotency-Keydeduplication and the/queueoperations endpoints.redis=...: one setting wires both the live event stream and cross-replica cancellation, so runs can be resumed and cancelled from any replica. Redis is coordination, never truth. A Redis fault degrades the live view; it cannot lose or corrupt a run.
External framework agents (LangGraph, Claude, etc.) stream inline when
background=true is requested, so their runs are not resumable. See
AgentOS Background Execution.
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.
Stop application writes while re-keying and keep them stopped until the report has been reviewed. This applies whether you call the learning helper directly or reach it through MigrationManager or the AgentOS migration endpoints.
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:
from agno.learn.migrations import rekey_user_entity_learnings
# Only needed if you are not running the full v3.0.0 migration.
# dry_run=True is the default: it reports what would change without writing.
print(rekey_user_entity_learnings(db))
# Apply it once the dry run looks right
result = rekey_user_entity_learnings(db, dry_run=False)
print(result["rekeyed"], result["merged"], result["failed"])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.
Legacy-keyed 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.
Already user-keyed mixed rows are reported as contaminated_keyed and remain in
place, even with purge_unrecoverable=True. Inspect contaminated_keyed,
malformed, and unowned alongside conflicts and failed before resuming
writes. MongoDB may have overwritten the owner evidence during a historic
collision, so zero detected contamination does not prove that no collision
occurred. The helper cannot automatically separate every mixed-user record.
Two API changes come with it:
delete/adeletetake a keyword-onlyuser_idand refusenamespace="user"deletes without it. Previously any caller could delete another user's entity by name.get/agetrequire auser_idin that namespace instead of returning an arbitrary user's row.
10. Smaller changes
- AgentOS metadata routes:
GET /modelswas removed (its data moved intoGET /configunderavailable_models), andGET /is now a minimal landing response.GET /infois 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 keepNULLprovenance and no data is rewritten, so this needs no action beyond running the migration. If you query the schedules table directly withSELECT *, expect the extra columns. update_scheduleis restricted to a column allow-list: it now writes onlyname,description,method,endpoint,payload,cron_expr,timezone,timeout_seconds,max_retries,retry_delay_seconds,enabled,next_run_atanddisabled_reason. Passing a provenance column raises aValueErrorinstead of silently repointing the row's owner or target.user_idis not an update field either: it scopes the update to that owner, so an update passing the wronguser_idmatches nothing.- Removed toolkit methods:
DuckDuckGoTools.duckduckgo_search->web_searchandduckduckgo_news->search_news;FileTools.check_escape->Toolkit._check_path;PgVector.enable_prefix_matchingremoved (dead helper);BrightDataTools.get_screenshotno longer takesoutput_path. - Removed learn aliases:
MemoriesConfig->UserMemoryConfig,MemoriesStore->UserMemoryStore,Decision->DecisionLog. - Eval result files:
store_result_in_file'seval_idparameter is nowrun_id, and{eval_id}is no longer accepted infile_path_to_save_resultstemplates -- use{run_id}.POST /eval-runsreturns the id the row was stored under. Workspacerefuses 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 withWorkspace(".", allow_paths=["config/credentials.json"]). Committed templates such as.env.examplebecome readable.- Studio memory forms:
enable_agentic_memoryandmemory_manager_idare gone from the Studio create/edit forms. Uselearning_name(a registry machine) orenable_learning=True. TheAgent/Teamconstructor parameters are unchanged, so stored configs keep rehydrating. - SQLite attempts WAL mode:
SqliteDb/AsyncSqliteDbenable WAL when available, which can create-waland-shmsidecar files. For a live database, use SQLite's backup API or another consistent snapshot method. Ordinary copies of active database and sidecar files can capture inconsistent state. MultiMCPToolsremoved: use oneMCPToolsper server. Theallow_partial_failureparameter 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 ofagno.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(olderseltzSDKs still work through a fallback;seltz>=1.2.0is needed for thescope, domain and date filters);BrandfetchToolsdropsasync_tools;StudioTool->StudioTools;GDriveContextProvider->GoogleDriveContextProvider. - AgentOS MCP config:
AgentOS(enable_mcp_server=..., mcp_config=...)->mcp=(a bool orMCPConfig). The earliermcp_server=andMCPServerConfigspellings remain deprecated compatibility aliases. - Removed model APIs: the
agno.models.metricsmodule and itsMetricsalias are gone -- useagno.metrics/RunMetrics.Model.classify_error->ModelProviderError.classify(error). LanceDb.use_tantivyis removed; passing it now raises aTypeError.- Pagination is validated:
pagewithoutlimit, orpage < 1, now raises aValueErrorinstead 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 withpip install -U "agno[mistral]". - Cerebras default model:
CerebrasandCerebrasOpenAInow default togpt-oss-120binstead ofllama-4-scout-17b-16e-instruct. Pin the old id explicitly if you depend on it. agno[postgres]installs a working driver: the extra previously installedpsycopg-binaryonly, soPostgresDbfailed withModuleNotFoundError: No module named 'sqlalchemy'. It now pullspsycopgandsqlalchemy; 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.
The prompt deliberately refuses to run the destructive cleanup step. Keep it that way: compare the complete migrated session and run history with the legacy records and retain a restorable backup before reclaiming legacy storage.