Skip to content

Migration Guide: Index v1 to v2

Migrate LlamaCloud Index v1 pipelines, data sources, and data sinks to Index v2 directories and indexes, with feature parity, endpoint and parameter mappings, and scheduled sync.

You only need this page if you have existing Index v1 pipelines. If you are starting fresh, use the getting started guide.

Index v1 (pipelines, data sources, data sinks) is deprecated. Index v2 uses a simpler model: you put files in a directory, create an index over that directory, and query it with the retrieval and chat APIs.

This guide covers what maps over, what changed, what v2 doesn’t support, and how to move an existing pipeline.

  • Index v2 needs a Starter, Pro, or Enterprise plan.
  • Python SDK: pip install "llama-cloud>=2.8". TypeScript SDK: npm install @llamaindex/llama-cloud.
  • All Index v2 SDK methods live under client.beta.*: client.beta.directories, client.beta.indexes, client.beta.retrieval, and client.beta.chat.
  • v1 and v2 don’t share storage. Migrating means adding your files to v2, which parses, chunks, and embeds them again, so plan for the parse credits.
Index v1Index v2Notes
PipelineIndexOne index per source directory.
Data source (connector or file upload)DirectoryUpload files into a directory, or sync them into it from a connected folder (SharePoint, Google Drive, S3).
Data sink (Pinecone, Qdrant, Postgres, …)Managed vector store, or a custom exportv2 writes to a vector store LlamaCloud manages. To keep the data in your own database, export the parsed output and load it yourself. See Replace data sinks with a custom export.
sync_interval on a data sourcePOST /api/v1/indexes/{index_id}/syncYou trigger syncs. See Sync an index on a schedule.
transform_config / embedding_configNoneLlamaCloud manages chunking and embedding; neither is configurable.
llama_parse_parametersSaved Parse configuration passed in productsDefaults to the cost_effective tier.
Pipeline file custom_metadataDirectory file metadataFilterable at query time with custom_filters.
pipelines.retrieveretrieval.retrieveHybrid search, with reranking on by default.
Retrieval modes files_via_metadata / files_via_contentretrieval.find / grep / readFile operations inside an index.
Pipeline chatchat sessionsStreaming chat agent, up to 10 indexes per session.

Legend: ✅ supported · 🔄 supported, works differently · ❌ not supported

Capabilityv1v2
File upload via APIclient.beta.directories.files.upload / .add
SharePoint🔄 connected folder, set up in the web app
Google Drive🔄 connected folder, set up in the web app
Amazon S3🔄 connected folder, set up in the web app
OneDrive
Box
Confluence
Jira
Azure Blob Storage
Insert raw text documents (pipelines.documents.create)❌ upload files instead
Per-file custom metadatametadata on directory files

For a source v2 doesn’t support, pull the files with your own code (for example an open-source LlamaIndex reader or the provider’s SDK) and upload them to the directory. Running that script on a schedule does the job a v1 connector did.

Capabilityv1v2
Parse settingsllama_parse_parameters🔄 saved Parse configuration referenced from products
Configurable chunkingtransform_config❌ fixed: sentence-based, 1024 tokens per chunk, 200-token overlap
Choice of embedding model / bring your own key✅ OpenAI, Azure, Bedrock, Cohere, Gemini, HuggingFace❌ managed by LlamaCloud
Change detection on sync✅ unchanged files are skipped; a change to metadata alone doesn’t parse the file again
Capabilityv1v2
Managed vector store✅ default
Your own vector database as a data sink✅ Pinecone, Qdrant, Milvus, Postgres, MongoDB Atlas, Azure AI Search, AstraDB❌ use a custom export
Capabilityv1v2
Dense (vector) search
Hybrid (vector + keyword)✅ default, with tunable weights
Reranking✅ on by default
Metadata filtersMetadataFilters🔄 custom_filters (eq, ne, gt, lt, gte, lte, in, nin)
Filter by filestatic_filters on parsed_directory_file_id
Find, grep, and read files in an index✅ new
Page screenshots returned with resultsretrieve_page_screenshot_nodes🔄 create the index with store_attachments=["screenshots"]. Each result lists the screenshots of the pages it covers, and you fetch them from the attachments endpoint. Search itself is text-only.
Figure-level image retrieval (retrieve_page_figure_nodes)store_attachments=["items"] stores structured items with bounding boxes, but figures aren’t returned as separate results
Composite retrieval (several indexes, one call)client.retrievers❌ query each index and merge the results, or use a chat session over several indexes
Auto-routed retrieval mode
Chat agent🔄 basic✅ streaming agent with sessions, up to 10 indexes; it can look at page screenshots when the index stores them
LlamaCloudIndex framework integration❌ call the retrieval API directly
Chunk-level listing (documents/{id}/chunks)

Index v2 REST paths under /api/v1/indexes, /api/v1/retrieval, and /api/v1/chat have no beta segment, but the SDK methods for them all live under client.beta.

v1 routev2 routev2 SDK method
POST /api/v1/data-sourcesPOST /api/v1/beta/directoriesclient.beta.directories.create
POST /api/v1/pipelines/{id}/filesPOST /api/v1/beta/directories/{id}/files (existing file_id) or .../files/upload (multipart)client.beta.directories.files.add / .upload
GET /api/v1/pipelines/{id}/filesGET /api/v1/beta/directories/{id}/filesclient.beta.directories.files.list
DELETE /api/v1/pipelines/{id}/files/{file_id}DELETE /api/v1/beta/directories/{id}/files/{file_id}client.beta.directories.files.delete
POST /api/v1/data-sinksnoneuse a custom export
PUT /api/v1/pipelines (create / upsert)POST /api/v1/indexesclient.beta.indexes.create
GET /api/v1/pipelinesGET /api/v1/indexesclient.beta.indexes.list
GET /api/v1/pipelines/{id} and /statusGET /api/v1/indexes/{id}client.beta.indexes.get (status in metadata["status"])
DELETE /api/v1/pipelines/{id}DELETE /api/v1/indexes/{id}client.beta.indexes.delete
POST /api/v1/pipelines/{id}/syncPOST /api/v1/indexes/{id}/syncclient.beta.indexes.sync
POST /api/v1/pipelines/{id}/retrievePOST /api/v1/retrieval/retrieve (index_id in the body)client.beta.retrieval.retrieve
retrieve_page_screenshot_nodes on pipelines/{id}/retrieveGET /api/v1/beta/attachments/{attachment_name}?source_id=... (presigned URL) and GET /api/v1/beta/attachments?source_id=... (list)none yet; call the REST endpoint
nonePOST /api/v1/retrieval/files/find / grep / readclient.beta.retrieval.find / grep / read
POST /api/v1/pipelines/{id}/chatPOST /api/v1/chat, then POST /api/v1/chat/{session_id}/messages/stream (SSE)client.beta.chat.create / .stream
POST /api/v1/retrievers/{id}/retrievenonenot supported

Deleting an index removes its sync and export configuration. The source directory and its files stay.

The examples below use the async Python SDK. The TypeScript SDK has the same method names and takes an options object.

from llama_cloud import AsyncLlamaCloud
client = AsyncLlamaCloud() # reads LLAMA_CLOUD_API_KEY
directory = await client.beta.directories.create(
name="product-docs",
description="Migrated from an Index v1 pipeline",
)

SharePoint, Google Drive, or S3 source: create a connected folder in the LlamaCloud web app instead, from the Files page or from Connect a folder when creating an index. LlamaCloud syncs the folder’s contents into a directory for you. The public API can’t set up connected folders yet.

Upload files directly, attaching the metadata you set as custom_metadata in v1:

from pathlib import Path
for path in Path("./docs").glob("*.pdf"):
await client.beta.directories.files.upload(
directory.id,
upload_file=path,
display_name=path.name,
unique_id=str(path), # stable ID, so a re-upload replaces the file instead of duplicating it
metadata='{"department": "legal", "year": 2026}', # JSON string on upload
)

Or add a file you already uploaded with client.files.create:

file_obj = await client.files.create(file=open("report.pdf", "rb"), purpose="user_data")
await client.beta.directories.files.add(
directory.id,
file_id=file_obj.id,
metadata={"department": "legal"},
)

Step 3: Save a Parse configuration (optional)

Section titled “Step 3: Save a Parse configuration (optional)”

An index uses the Parse cost_effective tier by default. To use other settings in place of v1’s llama_parse_parameters, save a Parse configuration:

parse_config = await client.configurations.create(
name="Index parse config",
parameters={"product_type": "parse_v2", "version": "latest", "tier": "agentic"},
)
index = await client.beta.indexes.create(
source_directory_id=directory.id,
name="product-docs",
# Omit `products` to use the default Parse configuration.
products=[{"product_type": "parse", "product_config_id": parse_config.id}],
# Store per-page screenshots, the v2 replacement for v1 page screenshot nodes.
# Add "items" to also store structured items with bounding boxes.
store_attachments=["screenshots"],
)

Set store_attachments when you create the index. An index can’t be updated after creation, so adding screenshots later means creating a new index over the same directory.

Creating an index starts its first sync, which parses, chunks, embeds, and stores every file in the directory.

import asyncio
while True:
idx = await client.beta.indexes.get(index.id)
status = idx.metadata.get("status")
if status == "ready":
break
if status == "failed":
raise RuntimeError(idx.metadata.get("error_message"))
await asyncio.sleep(10)

Only ready and failed are stable status values. Treat any other value as still in progress.

A v1 retrieval call:

results = await client.pipelines.retrieve(
pipeline_id=PIPELINE_ID,
query="What is the refund policy?",
dense_similarity_top_k=10,
sparse_similarity_top_k=10,
alpha=0.5,
enable_reranking=True,
rerank_top_n=5,
search_filters={"filters": [{"key": "department", "value": "legal", "operator": "=="}]},
)
for node in results.retrieval_nodes:
print(node.score, node.node.text)

The same call in v2:

results = await client.beta.retrieval.retrieve(
index_id=index.id,
query="What is the refund policy?",
top_k=5,
num_candidates=100,
vector_pipeline_weight=0.5,
full_text_pipeline_weight=0.5,
rerank={"enabled": True, "top_n": 5},
custom_filters={"department": {"operator": "eq", "value": "legal"}},
)
for r in results.results:
print(r.score, r.rerank_score, r.content)
print(r.static_fields.parsed_directory_file_id, r.static_fields.page_range_start)

v1 retrieval parameters map to v2 like this:

v1v2
pipeline_idindex_id
dense_similarity_top_k / sparse_similarity_top_knum_candidates (candidate pool for each search) and top_k (results returned, max 500)
alphavector_pipeline_weight / full_text_pipeline_weight (defaults 0.5 / 0.5)
enable_rerankingrerank.enabled (default true)
rerank_top_nrerank.top_n
dense_similarity_cutoffscore_threshold
search_filters ({"filters": [...]})custom_filters, keyed by metadata field
filter on a file IDstatic_filters on parsed_directory_file_id
retrieve_page_screenshot_nodes=Truestore_attachments=["screenshots"] at index creation, then results.results[i].static_fields.attachments
results.retrieval_nodes[i].node.textresults.results[i].content

See Retrieval for every v2 parameter.

Step 7: Fetch page screenshots for retrieval results

Section titled “Step 7: Fetch page screenshots for retrieval results”

When the index stores screenshots, each result’s static_fields.attachments lists one entry for every page the chunk covers, from page_range_start to page_range_end:

{"type": "screenshot", "attachment_name": "screenshots/page_7.jpg", "source_id": "dfl-..."}

GET /api/v1/beta/attachments/{attachment_name}?source_id={source_id} returns a presigned URL for the image. The SDKs don’t wrap this endpoint yet, so call it over HTTP:

import os
import httpx
API_BASE = "https://api.cloud.llamaindex.ai" # EU: https://api.cloud.eu.llamaindex.ai
headers = {"Authorization": f"Bearer {os.environ['LLAMA_CLOUD_API_KEY']}"}
async with httpx.AsyncClient(headers=headers) as http:
for r in results.results:
for att in r.static_fields.attachments or []:
if att.type != "screenshot":
continue
resp = await http.get(
f"{API_BASE}/api/v1/beta/attachments/{att.attachment_name}",
params={"source_id": att.source_id},
)
resp.raise_for_status()
image_url = resp.json()["url"] # presigned URL to the page JPEG

GET /api/v1/beta/attachments?source_id=... lists every attachment stored for a file.

Step 8: Replace file-level retrieval modes

Section titled “Step 8: Replace file-level retrieval modes”

The v1 files_via_metadata and files_via_content modes map to v2 file operations:

found = await client.beta.retrieval.find(index_id=index.id, file_name_contains="quarterly")
file_id = found.items[0].file_id
matches = await client.beta.retrieval.grep(
index_id=index.id, file_id=file_id, pattern="revenue|profit", context_chars=100,
)
text = await client.beta.retrieval.read(
index_id=index.id, file_id=file_id, offset=0, max_length=5000,
)

Run the same evaluation queries against the v1 pipeline and the v2 index before you switch traffic. v2 chunks and embeds differently, so scores and ranking will change. Delete the v1 pipeline once you’re happy with the v2 results.

Index v2 doesn’t support external data sinks: Pinecone, Qdrant, Milvus, Postgres, MongoDB Atlas, Azure AI Search, and AstraDB can’t be attached to an index. To keep the data in your own database, use a custom export. LlamaCloud still manages the directory, parsing, and sync; you chunk, embed, and write to your database.

  1. Create a parse-only index with vector_target="DISABLED":

    index = await client.beta.indexes.create(
    source_directory_id=directory.id,
    vector_target="DISABLED",
    )
  2. After each sync, download the parsed output from the index’s output directory. The source directory holds your original files; the index writes one parsed JSON payload per source file into the directory named by output_directory_id:

    import httpx
    idx = await client.beta.indexes.get(index.id)
    async with httpx.AsyncClient() as http:
    async for f in client.beta.directories.files.list(idx.output_directory_id):
    if f.file_id is None:
    continue
    presigned = await client.files.get(f.file_id)
    pages = (await http.get(presigned.url)).json()["parse"]["pages"]
    # f.id -> parsed_directory_file_id, stable across syncs
    # f.metadata -> the source file's metadata
    # f.updated_at -> changes when the file is parsed again or its metadata changes
  3. Chunk, embed, and write to your store:

    • Key records on (f.id, idx.export_config_id). Delete a file’s existing records before inserting its new chunks, so a file that’s parsed again replaces its old chunks instead of duplicating them.
    • Export only what changed: pass your last export time as updated_at_on_or_after when listing the output directory.
    • Delete records whose parsed_directory_file_id no longer appears in the output directory. A sync removes the output file when its source file is deleted, but LlamaCloud can’t clean up your database for you.

The index-v2-data-sinks repo has runnable exporters for MongoDB Atlas, PostgreSQL + pgvector, Qdrant, Pinecone, Turbopuffer, and Azure AI Search. They’re built on a shared Exporter protocol (export, delete_file, delete_files, list_snapshots); copy the one closest to your stack.

With vector_target="DISABLED" there is no managed vector store, so retrieval.*, chat, and the file operations have nothing to query. You serve retrieval from your own database. See Custom vector stores for the full walkthrough.

v1 synced data sources on a schedule set by sync_interval (6, 12, or 24 hours). Index v2 has no built-in schedule: an index syncs once when you create it, and after that whenever you call sync.

  1. Compares the source directory with what the index last processed.
  2. Parses new and changed files, skipping unchanged ones by their content fingerprint and source_modified_at. A change to metadata alone updates the metadata without parsing the file again.
  3. Chunks, embeds, and stores the changed files, and deletes the chunks of files removed from the directory.

Changes to the directory (uploads, deletions, connected-folder updates) only reach the index on the next sync.

Only one sync runs per index at a time: a second call while one is running returns 409 Conflict, and calling sync in rapid succession returns 429 Too Many Requests. Read last_synced_at before you trigger the sync, so the previous sync’s ready status isn’t mistaken for the new one:

import asyncio
from llama_cloud import ConflictError, RateLimitError
async def sync_and_wait(client, index_id, timeout_s=3600):
before = (await client.beta.indexes.get(index_id)).last_synced_at
try:
await client.beta.indexes.sync(index_id)
except ConflictError:
pass # a sync is already running; wait for it instead
except RateLimitError:
await asyncio.sleep(60)
await client.beta.indexes.sync(index_id)
delay, waited = 2, 0
while waited < timeout_s:
idx = await client.beta.indexes.get(index_id)
status = idx.metadata.get("status")
if status == "failed":
raise RuntimeError(idx.metadata.get("error_message"))
if status == "ready" and idx.last_synced_at != before:
return idx
await asyncio.sleep(delay)
waited += delay
delay = min(delay * 2, 30)
raise TimeoutError(f"index {index_id} did not finish syncing")

See Syncing for the sync endpoint in every SDK.

Replace sync_interval with a scheduled job

Section titled “Replace sync_interval with a scheduled job”

Run sync_and_wait on a schedule you control, such as cron, a Kubernetes CronJob, Airflow, or a scheduled GitHub Actions workflow. A daily or 12-hourly schedule matches what most v1 pipelines used.

  • Connected folders (SharePoint, Google Drive, S3): LlamaCloud refreshes a connected folder from its source periodically, but that refresh doesn’t sync the index. Keep the scheduled indexes.sync call so new and changed files reach the index.
  • Sources v2 doesn’t support: have the same scheduled job pull changed files from the source, upload them to the directory with the same unique_id so they replace the old version, delete files that are gone, and then call sync.
  • List your v1 pipelines with their data sources, data sinks, and sync intervals.
  • Check each data source against the ingestion feature parity table. For anything other than SharePoint, Google Drive, S3, or file upload, plan an upload script.
  • For each pipeline that writes to your own vector database, plan a custom export.
  • If you used page screenshot retrieval, create the v2 index with store_attachments=["screenshots"]. It can’t be added later.
  • If you relied on composite retrieval, figure-level image retrieval, custom chunking, or a specific embedding model, confirm the v2 behavior works for you before migrating.
  • Create directories and add files, carrying over custom_metadata.
  • Create indexes, with a saved Parse configuration if needed, and wait for ready.
  • Port retrieval calls using the parameter mapping.
  • Schedule indexes.sync calls to replace sync_interval.
  • Compare retrieval quality between v1 and v2 on your evaluation queries.
  • Switch traffic to v2, then delete the v1 pipelines.
Note for AI agents: this documentation is built for programmatic access. - Overview of all docs: https://developers.llamaindex.ai/llms.txt - Any page is available as raw Markdown by appending index.md to its URL — e.g. https://developers.llamaindex.ai/llamaparse/parse/getting_started/index.md - Agent-friendly REST search APIs live under https://developers.llamaindex.ai/api/ — search (BM25 full-text), grep (regex), read (fetch a page), and list (browse the doc tree). See https://developers.llamaindex.ai/llms.txt for parameters. - A hosted documentation MCP server is available at https://developers.llamaindex.ai/mcp. If you support MCP, you can ask the user to install it for browsing these docs directly (an alternative to the REST API). Setup: https://developers.llamaindex.ai/for-agents/mcp/ - Other LlamaIndex tooling for agents — the LlamaParse Platform MCP server, agent skills and plugins, and the n8n node — is mapped at https://developers.llamaindex.ai/for-agents/