Sync SaaS Data to Pinecone for RAG: Upserts & Namespaces
Sync live SaaS data into Pinecone for RAG with Truto sync jobs: upsert vectors or documents, isolate each customer by namespace, and delete stale records.
You need to feed SaaS data into Pinecone for RAG with Truto sync jobs: upsert vectors or documents, query by namespace, and keep a per-customer vector index fresh. Doing this in a local Jupyter notebook with a handful of static PDFs is a weekend project. Doing it in a production B2B SaaS environment - where you must sync live customer data from external SaaS apps into Pinecone without building custom ETL infrastructure - is a much harder problem.
Building a real-time data pipeline from enterprise SaaS apps requires moving past simple data ingestion scripts. You need an architecture that handles OAuth token refreshes, normalizes deeply nested JSON payloads, runs incremental syncs, and respects strict tenant isolation boundaries.
This guide breaks down how to architect an ingestion layer that pulls data from CRMs, ticketing systems, and HRIS platforms into your Pinecone index without drowning your engineering team in custom ETL maintenance.
The RAG Data Freshness Problem in Enterprise SaaS
Most LangChain and LlamaIndex tutorials assume your knowledge base is a static directory of Markdown files. In enterprise reality, your users store their knowledge in fragmented, constantly updating SaaS platforms like Salesforce, Zendesk, Jira, and Confluence.
If you build a RAG pipeline that takes a one-time snapshot of this data, your AI agent will soon begin providing incorrect answers based on stale context.
Stale context looks like hallucination to the end user: the model answers confidently from a version of the record that no longer exists. When a support agent updates a Zendesk ticket or a sales rep closes a Salesforce opportunity, that state change needs to reach your vector database on the next sync.
To solve this, developers often attempt to build custom polling scripts or webhook listeners. But as we covered in our guide on how to architect a real-time SaaS-to-vector DB pipeline, building these pipelines in-house means absorbing the maintenance burden of hundreds of undocumented API edge cases.
Consider the alternative architectures. Tools like n8n position themselves as low-code visual workflow orchestrators for RAG pipelines, but you still wire up the chunking logic, embedding API calls, and exception handling for Pinecone syncs node by node.
Streaming CDC pipelines like Estuary focus on real-time syncs from operational databases to vector stores, but SaaS apps do not give you direct database access. You must consume their REST APIs, which means OAuth token management and pagination on top of your embedding step.
Frameworks like LangGraph provide the agentic framework to query Pinecone and build stateful agents, but they leave the actual data ingestion and continuous synchronization from SaaS tools up to the developer.
Multi-Tenant Isolation with Pinecone Namespaces
When you sync SaaS data to Pinecone for multiple customers, security is your primary constraint. You cannot allow Customer A's AI agent to semantically retrieve chunks from Customer B's Confluence wiki.
Pinecone namespaces partition records within a single index, and Pinecone recommends them for separating organizations or users. Instead of standing up a dedicated Pinecone index for every customer - which is costly and operationally complex - you use a single index and partition the data logically.
In Truto, the namespace is a parameter of the datastore method, so you choose it in the sync job's destination config. Each of your customers connects their SaaS app as an integrated account, and the sync job runs with that account's id in its args. Set namespace to {{args.integrated_account_id}} (or another stable customer key), and every record from that customer lands in that customer's namespace. Truto does not pick a namespace for you; when you leave it out, Pinecone's __default__ namespace is used.
Scope your queries too. Namespaces only isolate data if every read names the same namespace. Make sure your retrieval code always passes the customer's namespace to query or searchDocuments.
How Truto Sync Jobs Feed SaaS Data into Pinecone
To orchestrate a Pinecone RAG pipeline from SaaS apps, you need an ingestion layer that decouples the extraction of data from the loading of vectors. Truto separates the two: a datastore holds the connection credentials (encrypted at rest), while a sync job destination defines which Pinecone method to call and with what parameters.
The Datastore Abstraction
A datastore in Truto is a first-class entity representing a connection to an external storage system. A Pinecone datastore needs two config fields: api_key (encrypted at rest) and index_host, the index URL from the Pinecone console. The index_host must be a public https URL with no query or fragment.
{
"type": "pinecone",
"label": "rag-index",
"config": {
"api_key": "<your Pinecone API key>",
"index_host": "https://rag-index-abc123.svc.aped-4627-b74a.pinecone.io"
}
}When you create the datastore, you can ask Truto to validate it with testConnection, which calls Pinecone's describe_index_stats. testConnection works on both classic and document indexes, so a passing test does not tell you which kind of index you have. API responses never return the api_key.
Because the datastore only holds the how to connect, you can reuse a single Pinecone datastore across many sync jobs. One sync job might pull Jira tickets, another might pull Zendesk articles, and both push to the same Pinecone index with different destination configs. Truto sends Pinecone requests directly to your index_host; they do not pass through a Truto proxy.
flowchart TD
subgraph SaaS ["External SaaS Apps"]
SF["Salesforce"]
ZD["Zendesk"]
CF["Confluence"]
end
subgraph TrutoPlatform ["Truto Platform"]
SyncJob["Sync Job (V4 runtime)"]
Datastore["Pinecone Datastore<br>(credentials encrypted at rest)"]
SyncJob -->|"Uses credentials from"| Datastore
end
subgraph VectorDB ["Pinecone"]
Index["Index"]
NS1["Namespace: Customer A"]
NS2["Namespace: Customer B"]
Index --- NS1
Index --- NS2
end
SF -->|"Unified API"| SyncJob
ZD -->|"Unified API"| SyncJob
CF -->|"Unified API"| SyncJob
SyncJob -->|"upsertDocuments / upsert"| NS1
SyncJob -->|"upsertDocuments / upsert"| NS2This architecture removes the complexities of content retrieval and pagination from APIs like Confluence or Notion. The sync job's request nodes call Truto's unified APIs, which return records in a common schema, and a transform node reshapes them before the destination node hands them to the Pinecone datastore client.
Who Computes the Embeddings
Truto does not compute embeddings or chunk text. What you send depends on the kind of Pinecone index:
| Index type | Truto method | What each record must contain | Who embeds |
|---|---|---|---|
| Document index with an embedded field | upsertDocuments |
An _id plus the fields in the index's document schema |
Pinecone, on its side |
| Classic vector index | upsert |
An id, the vector values, and optional metadata |
You, with your own embedding model, before the destination runs |
For a classic index, the vectors must already be in the sync job's context when the destination runs. Truto only moves them to Pinecone. If you do not want to run an embedding step yourself, create a document index whose schema embeds a text field, and let Pinecone do the work.
Defining the Sync Job Destination
A V4 sync job is a graph of typed nodes. Request nodes pull records from a unified API, transform nodes reshape them with JSONata, and destination nodes write them out. To sync Zendesk tickets into a Pinecone document index, a transform builds the documents array:
{
"name": "tickets-to-pinecone-documents",
"type": "transform",
"depends_on": "list-tickets",
"config": {
"expression": "{ 'documents': resources.ticketing.tickets.{ '_id': id, 'text': subject & '\\n\\n' & description, 'source': 'zendesk', 'updated_at': updated_at }[] }"
}
}Then a datastore destination calls upsertDocuments on the Pinecone datastore:
{
"name": "pinecone-documents",
"type": "destination",
"destination_type": "datastore",
"method": "upsertDocuments",
"config": {
"id": "{{args.pinecone_datastore_id}}",
"config": {
"namespace": "{{args.integrated_account_id}}",
"documents": "{{payload.records.0.documents:json}}",
"batch_size": 96
}
},
"resources_to_persist": ["tickets-to-pinecone-documents"]
}The runtime replaces the {{...}} placeholders against the sync context, then calls the method with the resolved config. The :json suffix keeps the documents as an array instead of a string. The field names in each document (text, source, updated_at) must match your index's document schema.
For a classic vector index, the destination calls upsert instead, with vectors in place of documents. Each vector needs its values from your embedding step:
{
"method": "upsert",
"config": {
"id": "{{args.pinecone_datastore_id}}",
"config": {
"namespace": "{{args.integrated_account_id}}",
"vectors": "{{payload.records.0.vectors:json}}"
}
}
}Handling Upserts and Deletes
Writing vectors to Pinecone requires understanding the underlying index type and limits. Pinecone supports classic vector indexes and document indexes, and Truto's Pinecone client supports both. For document indexes, it uses Pinecone's documents API (X-Pinecone-API-Version: 2026-07).
An index created with a document schema rejects every /vectors/* and /query call. It only accepts Pinecone's documents API. If you send a classic upsert to a document index, Pinecone rejects it.
Truto's datastore client therefore exposes separate methods for each kind of index:
| Classic vector index | Document index |
|---|---|
upsert({ vectors, namespace, batch_size }) |
upsertDocuments({ documents, namespace, batch_size }) |
query (Pinecone's fields, e.g. topK, includeMetadata) |
searchDocuments({ namespace, score_by, top_k, include_fields, filter }) |
deleteVectors (Pinecone's fields, e.g. ids, filter, deleteAll) |
deleteDocuments({ namespace, ids | filter | delete_all }) |
For the document methods, the namespace goes in the request URL and defaults to __default__. The rest of the body is passed through in Pinecone's own field names.
Batching for Upserts
Both upsert methods split the records into batches and send them one batch at a time.
upsertsends batches of 100 vectors by default, up to 1,000 per batch.upsertDocumentssends batches of 96 documents by default, which is Pinecone's limit when the index embeds a field. You can raisebatch_sizeto 1,000 for an index that does not embed.
If a batch fails, the method stops at that batch. The failure message states how many records the earlier batches upserted, so you know the earlier batches are already in Pinecone. The sync job then sends a sync_job_run:record_error webhook event, which you can subscribe to for alerting.
The Threat of Phantom Retrieval
Upserting data is only half the battle. Deleting data is where many RAG pipelines fail. If a record is deleted in the source SaaS app but its embeddings remain in the vector database, your agent can still retrieve it. We call this Phantom Retrieval.
If a document (like a confidential HR memo in Notion) is purged from the source system but its chunk embeddings remain in the vector index, the RAG system can keep surfacing that content in semantic searches.
A Pinecone destination does not remove records on its own. Your pipeline must detect the delete and then call deleteDocuments or deleteVectors for the affected records. Truto passes the request bodies through in Pinecone's own field names. You can delete by ids, or by a metadata filter that matches the deleted source record's ID if one source record produced several chunks.
{
"method": "deleteDocuments",
"config": {
"id": "{{args.pinecone_datastore_id}}",
"config": {
"namespace": "{{args.integrated_account_id}}",
"ids": "{{payload.records.0.deleted_ids:json}}"
}
}
}For a deeper dive into this security flaw, read our guide on how to handle deleted SaaS records in RAG pipelines to prevent data leaks.
Calling the Unified API from your own code? When an upstream SaaS API rate limits a direct Unified API call, Truto passes the 429 to your code with standard ratelimit-limit, ratelimit-remaining and ratelimit-reset headers, so your client can schedule its retry.
Orchestrating the Full Pipeline
Building the ingestion pipeline is the first step. Once your SaaS data is syncing into per-customer Pinecone namespaces, your AI agents need a way to access it.
Instead of hardcoding Pinecone SDK calls into your LangChain or Vercel AI SDK applications, you can expose these datastores back to your agents as tools. Read our guide to connect Pinecone to AI agents and manage vector indexes to learn how to give agents the tools to query the Pinecone namespaces your sync jobs populated.
With Truto handling both the extraction of SaaS data and the writes into Pinecone, you avoid custom ETL infrastructure, keep retrieval context fresh, and keep each customer's records in their own namespace.
FAQ
- How do you sync live SaaS data to Pinecone?
- Create a Pinecone datastore in Truto with your `api_key` and `index_host`. Then add a datastore destination to a sync job that calls `upsertDocuments` (document indexes) or `upsert` (classic vector indexes) with the records the job pulled from a unified API.
- Does Truto generate the embeddings?
- No. Truto does not compute embeddings. For the classic `upsert` method, each record must already carry vector values from your own embedding model. For a Pinecone document index with an embedded field, `upsertDocuments` sends text and Pinecone embeds it on its side.
- How do you handle tenant isolation in Pinecone?
- Set the `namespace` parameter in the destination config to a value that identifies the customer, such as `{{args.integrated_account_id}}`. Each customer's records then live in their own namespace, and your queries stay scoped to one namespace.
- What happens if a synced SaaS record is deleted?
- The Pinecone record stays until you delete it. Detect the delete in your pipeline and call `deleteDocuments` (with `ids`, `filter` or `delete_all`) or `deleteVectors` (in Pinecone's field names, such as `ids` or `deleteAll`) for that record.
- What happens when a Pinecone write fails during a sync job?
- The upsert stops at the first failed batch, and the failure states how many records the earlier batches upserted. The sync job then sends a `sync_job_run:record_error` webhook event.