Short answer: install cognee-community-connector-freshsales, set FRESHSALES_DOMAIN, FRESHSALES_API_KEY and LLM_API_KEY, then pass freshsales_source() to cognee.remember() with write_disposition="merge". Your contacts, accounts and deals, plus their notes, become a knowledge graph in Cognee, and every later sync only fetches what changed.

In Freshsales (Freshworks CRM), a deal's stage lives on the deal, the person lives on a contact, and what the customer actually said usually ends up in a note. A question like "which deals in negotiation got a discount, and who's the contact?" means opening all three kinds of record and reading a lot of free text. I wanted to type that question and get an answer, so I built a Freshsales connector for Mergetober, the open source month run by WeMakeDevs and Cognee.

Cognee (docs) is an open source memory layer for AI agents. You give it data with add, cognify turns that data into a knowledge graph (this step needs an LLM API key), and search asks the graph questions. A connector is a way to feed it a tool's data instead of a folder of files.

What does the Freshsales connector for Cognee do?

  • Turns every contact, account and deal into one document, with its notes attached oldest first and each note's author.
  • Replaces ids for stage, pipeline, owner, account and contacts with names, so a deal reads "Negotiation" instead of a number.
  • Syncs incrementally. After the first run it only fetches records changed since the last successful one.
  • Forgets what you delete in Freshsales on the next sync.

How do you connect Freshsales to Cognee?

You need cognee 1.4.0 or newer. Setup takes four steps:

  1. Install the package.
  2. In Freshsales, open your profile, go to Settings → API Settings and copy your API key.
  3. Find your domain. It's the part before .myfreshworks.com in your address bar (acme in https://acme.myfreshworks.com/crm/sales). Older acme.freshsales.io accounts work too.
  4. Export the variables, then sync and search.
uv pip install cognee-community-connector-freshsales export FRESHSALES_DOMAIN="acme" export FRESHSALES_API_KEY="..." export LLM_API_KEY="sk-..." 
import cognee from cognee_community_connector_freshsales import freshsales_source await cognee.remember( freshsales_source(), # or freshsales_source(entities=["deals"])  dataset_name="freshsales", write_disposition="merge", # REQUIRED ) answer = await cognee.search( query_text="Which deals are in negotiation and what do the notes say about pricing?", query_type=cognee.SearchType.GRAPH_COMPLETION, datasets=["freshsales"], ) 

One gotcha: write_disposition="merge" is required. Cognee defaults to replace, which reloads the table on every run. An incremental run only sees what changed, so replace would forget everything else.

What happened when I ran it on a real Freshsales account?

I used the sample records in a Freshsales trial account: 10 contacts, 9 accounts and 12 deals. I synced them, synced again without touching anything, then made three edits in Freshsales and synced a third time.

First edit: I moved the "ECorp renewal" deal ($4,000) to Negotiation.

Second, I deleted Nick Raymond, the contact on that deal. Freshsales asks you to confirm and says the contact stays in the Recycle Bin for 90 days.

Third, I added a note to the E Corp account: "Asked for a 3-year contract."

Here is what the connector did each time:

Sync What changed in Freshsales What the connector did API requests
1 Nothing, first run Pulled 31 records with their notes 34
2 Nothing Fetched 0 records and left all 31 unchanged 12
3 The three edits above Re-synced the deal and the account, forgot the deleted contact (31 → 30) 14

The re-synced deal showed Stage: Negotiation and Probability: 80%. Run 3 also showed that adding a note bumps its parent record's updated_at, so notes don't need a sync of their own.

What can you ask once your CRM is in memory?

After a sync, cognee.search answers questions against the graph. The query in the connector's README is "Which deals are in negotiation and what do the notes say about pricing?" After my three edits, I'd ask "Which deals are in negotiation?" and "What has E Corp asked for?", because the right answers changed with those edits.

[TODO: replace this line with your screenshot of Cognee's answers (upload it in the editor), plus a sentence or two on what it got right. Delete this line if you don't, because it will publish as written.]

How does the sync know what changed?

For each entity, the connector searches Freshsales with updated_at is_after <cursor>, sorted by updated_at, 100 records per page. The cursor is the UTC time the previous run started, and it only moves after a run finishes without errors. If a run dies halfway, the next one starts from the old cursor and nothing gets skipped.

Search results are partial. A deal comes back without a stage, an owner or even updated_at, so each changed record gets one detail call that also pulls its notes, owner, stage, account and contacts. Unchanged records cost nothing. A first sync costs about one request per record plus one per 100 records, and a run with no changes costs about four requests per entity.

What happens when you delete a record in Freshsales?

Freshsales moves it to the Recycle Bin of that entity. The connector reads the Recycle Bin view on every run and tells Cognee to forget those ids, and Cognee then removes them from the graph and the vector store. By default (reconcile=True) it also lists every id of the entity, one request per 100 records, and forgets any id it knew about that has disappeared. That covers records purged from the Recycle Bin. A run that hits an error aborts before forgetting anything, so a partial read can never delete data by mistake.

What surprised me about the Freshsales API?

Unknown filter operators return 200 and act like "equals"

To fetch changed records I filter on updated_at. greater_than looks like the obvious operator, and Freshsales doesn't know it. It doesn't say so either. The API answers HTTP 200 and quietly treats the filter as "equals", so you get almost nothing back and no error. A made-up operator called bogus_op behaved the same way. A sync that fetches nothing and reports success is easy to miss, so the operator is now fixed in code, and a test fails if anyone changes it. The one that works is is_after.

The filter is strict and timestamps have one-second precision

is_after is strict, so a record changed in the same second as the cursor could be missed. Each query now starts five minutes before the cursor. The overlap does no harm because records are merged, not duplicated.

Deal records contain auth tokens

Live deal records include chat-widget JWT auth tokens. If I had built documents from the raw records, those tokens would have ended up in a knowledge graph. Documents are built from an allow-list of fields instead. E-mail and phone fields are left out by default (redact_contact_details=False brings them back), and so are values that change without anyone editing the record, like deal age and lead score, which would make every record look changed on each sync. After the live test I searched all the synced documents for e-mails, phone numbers and tokens and found none.

The rate limits weren't what the docs said

The documentation I read said 1,000 requests per hour. The account's response headers reported 5,000 per hour and 400 per minute. The connector watches the per-minute budget in those headers and pauses when it runs low, and it retries 429 responses, 5xx errors and network failures.

Frequently asked questions

Does the connector send e-mails or phone numbers to Cognee?

Not by default. They're left out unless you pass redact_contact_details=False.

Which Freshsales data does it sync?

Contacts, accounts and deals, each with their notes. Pass entities=["deals"] to limit it, or include_notes=False to skip notes.

How many Freshsales API requests does a sync use?

About one per record plus one per 100 records for the first sync, and about four per entity when nothing changed. In my test that was 34 requests for 31 records, then 12 for the no-change run.

Can I remove the data from Cognee later?

Yes, if you sync into a dedicated dataset (the examples use dataset_name="freshsales"). One cognee.prune removes it.

Try it

The code is in cognee-community pull request #379, open for review. It has 16 passing tests. To check that they test something, I removed each safeguard one at a time (the safety window, the fixed operator, the allow-list, the Recycle Bin pass, the reconcile check, the cursor rule, the per-minute pause and the contact redaction), and every removal made at least one test fail. A Freshsales trial account is enough to run it yourself.

I opened four other connectors this month, for Readwise, Canny, Help Scout and Zoho CRM, and each gets its own post.

If you try the Freshsales one, tell me what you ask it and what breaks.

I used an AI assistant to help draft this post and to help write the connector code. I reviewed the code, ran the tests and ran the live test on my own Freshsales trial account, and the screenshots are mine.