Building a Trello Connector for cognee: The Card That Moved While Nobody Was Looking
How I turned Trello boards into searchable AI memory, and the bugs a careful reviewer found that my tests never would have.

Trello boards are full of decisions. Why a feature was cut, who owns the export bug, what the team agreed in a comment three weeks ago. All of it is in there, but the only way to find it is to click through cards one at a time.
During Mergetober, cognee’s month-long integration hackathon, I picked up issue #4702: a Trello data-source connector. The goal was simple to say. Point it at a board, and later ask cognee things like “What is blocking the export work and who owns it?”
This post walks through how the connector works, the design choices behind it, and what four rounds of code review taught me. The pull request is cognee-community #358.
A quick primer: cognee and dlt
If you have not used these before, here is all you need.
cognee is an open-source memory layer for AI agents. You give it documents with cognee.remember(...), it extracts entities and relationships into a knowledge graph, and cognee.recall(...) answers questions from that graph.
dlt (data load tool) is a Python library for moving data. A connector in the cognee community repo is a dlt resource: a generator that yields rows. cognee accepts that resource directly, which means the connector only has to answer three questions:
- What does one document look like?
- What changed since the last run?
- What was deleted and must be forgotten?
That third question turned out to be the hard one.
What it looks like to use
import os
import cognee
from cognee_community_connector_trello import trello_source
await cognee.remember(
trello_source(
board_ids=["<board id>"],
api_key=os.environ["TRELLO_API_KEY"],
token=os.environ["TRELLO_TOKEN"],
),
dataset_name="trello",
primary_key="id",
write_disposition="merge", # REQUIRED, the add pipeline defaults to "replace"
max_rows_per_table=0,
self_improvement=False,
)
answer = await cognee.recall("What is blocking the export work?", datasets=["trello"])
Run the same call again and only the changes are sent. That is the whole user-facing API.
One document per card, one per board
Every card becomes one document: its list, labels, members, start and due dates, description, checklists, and comments with their authors. Every board becomes one more document with its description, lists, labels and members.
Two small decisions here saved a lot of wasted work later:
- Card titles stay out of the board document. Otherwise adding a card would change the board document and make cognee re-process the whole board.
- The board name stays out of the card text. This one came from review. If every card said “Board: Product”, renaming the board would re-extract every card on it, even though nothing in them changed. The board name already lives on the board document and in each card’s node set (trello:<board id>), so cards do not need it.
Everything is sorted too, labels, members, lists and checklist items. Trello does not promise an order, and if the order changed between two runs, the document would look different and cognee would re-process it for nothing.
Knowing what changed
Here is the first surprise. Trello cards have no reliable “last updated” timestamp you can filter on.
So the connector uses the board’s actions feed as its cursor. Every change on a board creates an action, and actions have increasing ids. The connector stores the newest id it has seen, and on the next run asks for actions after it. A board with no new action and the same dateLastActivity costs just two requests and is skipped.
But the actions feed has gaps. Trello’s documentation lists action types it leaves out of nested feeds: checklist item changes, comment edits and deletions, and label changes. A cursor alone would miss all of those.
The fix: when a board did change, the connector re-reads its cards and checklists and compares each card against a fingerprint saved from the last run. The fingerprint includes the comment count, so an added or deleted comment shows up too, without reading a single comment.
Comments are only read for cards that actually changed. Sometimes that means one request per card, sometimes one request for the whole board’s comment feed, whichever costs fewer requests.
The bug a reviewer found at exactly 1,000
Trello’s card comment endpoint returns comments in pages of 50, pages 0 to 19, so at most 1,000 comments. For a card with more than that, the connector reads the board feed instead.
My code sent cards with <= 1000 comments to the card endpoint. The reviewer, goran-radonic, ran a card with exactly 1,000 comments: page 19 comes back full, and a full last page cannot tell "exactly 1,000" from "more than 1,000". The connector raised an error, and it would raise on every run until the comment count changed. 999 and 1,001 both worked.
The fix was one character, < instead of <=, plus a test for exactly 1,000. The lesson was bigger: test the boundary itself, not just both sides of it.
The card that moved while the sync was paused
This is the bug I am proudest of fixing, because it is subtle.
To avoid hammering the API, a run has a request budget. A big account cannot be synced in one run, so the connector syncs as many boards as the budget allows and continues next time. A card only counts as deleted once a pass has visited every board, so a card that just moved from one board to another is not lost.
The reviewer found a hole in that. Picture this:
Knowing what changed
Here is the first surprise. Trello cards have no reliable “last updated” timestamp you can filter on.
So the connector uses the board’s actions feed as its cursor. Every change on a board creates an action, and actions have increasing ids. The connector stores the newest id it has seen, and on the next run asks for actions after it. A board with no new action and the same dateLastActivity costs just two requests and is skipped.
But the actions feed has gaps. Trello’s documentation lists action types it leaves out of nested feeds: checklist item changes, comment edits and deletions, and label changes. A cursor alone would miss all of those.
The fix: when a board did change, the connector re-reads its cards and checklists and compares each card against a fingerprint saved from the last run. The fingerprint includes the comment count, so an added or deleted comment shows up too, without reading a single comment.
Comments are only read for cards that actually changed. Sometimes that means one request per card, sometimes one request for the whole board’s comment feed, whichever costs fewer requests.
The bug a reviewer found at exactly 1,000
Trello’s card comment endpoint returns comments in pages of 50, pages 0 to 19, so at most 1,000 comments. For a card with more than that, the connector reads the board feed instead.
My code sent cards with <= 1000 comments to the card endpoint. The reviewer, goran-radonic, ran a card with exactly 1,000 comments: page 19 comes back full, and a full last page cannot tell "exactly 1,000" from "more than 1,000". The connector raised an error, and it would raise on every run until the comment count changed. 999 and 1,001 both worked.
The fix was one character, < instead of <=, plus a test for exactly 1,000. The lesson was bigger: test the boundary itself, not just both sides of it.
The card that moved while the sync was paused
This is the bug I am proudest of fixing, because it is subtle.
To avoid hammering the API, a run has a request budget. A big account cannot be synced in one run, so the connector syncs as many boards as the budget allows and continues next time. A card only counts as deleted once a pass has visited every board, so a card that just moved from one board to another is not lost.
The reviewer found a hole in that. Picture this:

- Run 1 reads board A. Card C is on board B, so it is not on A. Then the budget runs out.
- Between runs, someone moves card C from board B to board A.
- Run 2 reads board B. Card C is not there anymore either.
The pass is now complete, and card C is on no snapshot. The old code deleted it, while it was sitting right there on board A. Each snapshot was correct when it was taken; they were just taken at different times.
The fix: before forgetting any card that no board listed, the connector asks Trello directly with GET /cards/{id}. It keeps the card if it is now on a selected board and not archived. It only deletes on a 404, a board you did not select, or an archived card. If Trello answers 401, the connector checks the token once more, so an expired token is never mistaken for a deleted card.
I did not want to trust my test fake for this, so I checked the real API. On two temporary boards, I confirmed that the lookup returns the board’s and list’s archived flags, and that a deleted card answers 404.
Testing against the real API, not just my assumptions
The reviewer also asked a sharp question: when a card with more than 1,000 comments moves to another board, do its comments move with it, or stay in the old board’s feed? My test fake moved them, but a fake only proves what you told it.
So I tested it live. I made a card with three comments, moved it to another board, and checked both feeds. The old board’s feed had none of the comments; the new board’s had all three. The fake was right, and now the README says so with evidence behind it.
Here is the live run on two fresh boards, with the request count for each step:

Never forget data by mistake
A connector that deletes too eagerly is worse than one that deletes nothing. A few rules kept it safe:
- A 200 answer without the list it should contain raises. If Trello ever answers a board without its cards field, that must not read as "every card was deleted".
- An empty board listing waits. If the board list suddenly comes back empty while boards were synced before, nothing is forgotten until the next run confirms it.
- A board you listed that cannot be read raises instead of being forgotten.
- Every stop is reported honestly. A rate limit, an expired token or a spent budget stops the run cleanly, keeps what finished, and sets failed plus a reason in cognee_sync_stats. A caller who checks failed == 0 can trust it means "complete".
What four rounds of review taught me
The first review left 14 comments. Some were small, like sorting the card’s labels and members. Some were real problems: the default name for the sync state was the same for every sync, so two syncs in one dataset would have deleted each other’s cards. The actions feed request sent no filter, so it silently got a default set of action types.
By the last round, the remaining comments were the moved card and the 1,000 boundary, things the reviewer found by running the connector through a real dlt pipeline with a tight budget.
What I would tell anyone writing their first connector:
- Ask what your cursor cannot see. Every incremental sync has blind spots. Find them in the API docs before a reviewer does.
- Deleting is the dangerous part. Write down exactly when something counts as deleted, and check it against the source when in doubt.
- Test the edges, then test them live. My fake API was useful, but every time I checked the real API, I learned something.
- A request budget changes the problem. Once a sync spans several runs, “the state I read” and “the state right now” are different things.
The connector has 79 tests that run on Python 3.10, 3.11 and 3.13 without any Trello account. Each one exists because a specific bug could otherwise slip through.
Links
- Pull request: topoteretes/cognee-community #358
- Issue: topoteretes/cognee #4702
- cognee: github.com/topoteretes/cognee
Built for Mergetober 2026, cognee’s open-source integration hackathon.